如果你有一台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所加載的所有音效也會被釋放。
現在編寫一個簡單的測試活動,每當單擊屏幕時它就播放一個爆炸音效。代碼如下:
[java]
package org.example.ch04_android_basics;
import java.io.IOException;
import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.TextView;
public class SoundPoolTest extends Activity implements OnTouchListener{
SoundPool soundPool;
int explosionId = -1;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
TextView textView = new TextView(this);
textView.setOnTouchListener(this);
setContentView(textView);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
soundPool = new SoundPool(20, AudioManager.STREAM_MUSIC, 0);
try{
AssetManager assetManager = getAssets();
AssetFileDescriptor descriptor = assetManager
.openFd("explosion.ogg");
explosionId = soundPool.load(descriptor, 1);
}catch(IOException e){
textView.setText("Couldn't load sound effect from asset, " +
e.getMessage());
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
if(event.getAction() == MotionEvent.ACTION_UP){
if(explosionId != -1){
soundPool.play(explosionId, 1, 1, 0, 0, 1);
}
}
return true;
}
@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
soundPool.release();
}
}
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/目錄下的一個音頻文件。該音效將根據活動的生命周期實現暫停和恢復,當活動暫停時,音樂也要暫停;而活動恢復時,音樂也要從上次暫停的地方開始播放。
代碼如下:
[java]
package org.example.ch04_android_basics;
import java.io.IOException;
import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.widget.TextView;
public class MediaPlayerTest extends Activity {
MediaPlayer mediaPlayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
TextView textView = new TextView(this);
setContentView(textView);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
mediaPlayer = new MediaPlayer();
try{
AssetManager assetManager = getAssets();
AssetFileDescriptor descriptor = assetManager.openFd("music.ogg");
mediaPlayer.setDataSource(descriptor.getFileDescriptor(),
descriptor.getStartOffset(), descriptor.getLength());
mediaPlayer.prepare();
mediaPlayer.setLooping(true);
}catch(IOException e){
textView.setText("Couldn't load music file, " + e.getMessage());
mediaPlayer = null;
}
}
@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
if(mediaPlayer != null){
mediaPlayer.pause();
if(isFinishing()){
mediaPlayer.stop();
mediaPlayer.release();
} www.2cto.com
}
}
@Override
protected void onResume() {
// TODO Auto-generated method stub
super.onResume();
if(mediaPlayer != null)
mediaPlayer.start();
}
}
在onResume()方法中,我們只需啟動MediaPlayer(如果已經成功創建它)。onResume()方法是一個處理該操作的完美地方,因為它在onCreate()方法和onPause()方法之後被調用。在第一種情況下,它將第一次啟動播放;在第二種情況下,它將簡單地恢復已暫停的MediaPlayer。
在onPause()方法中,我們暫停MediaPlayer。如果該活動被銷毀,我們還需要停止該MediaPlayer並釋放所有資源