Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android開發筆記(一百二十六)自定義音樂播放器

Android開發筆記(一百二十六)自定義音樂播放器

編輯:關於Android編程

MediaRecorder/MediaPlayer

在Android手機上面,音頻的處理比視頻還要復雜,這真是出人意料。在前面的博文《Android開發筆記(五十七)錄像錄音與播放》中,介紹了視頻/音頻的錄制與播放,其中錄像用的是MediaRecorder類,播放用的是MediaPlayer類。雖然Android還提供了專門的視頻視圖VideoView,但是該控件並非新的東西,而是繼承了MediaRecorder和MediaPlayer,所以嚴格來說,Android上面只有一種視頻的錄制和播放方式。可是音頻就大不一樣了,Android提供了兩種錄音方式,以及至少三種常用的播音方式。兩種錄音方式分別是MediaRecorder類和AudioRecord類,而播音方式包括MediaPlayer類、AudioTrack類和SoundPool類,它們的使用場合各有千秋,且待筆者下面細細道來。

首先是MediaRecorder與MediaPlayer,這對組合即可用於錄像,也可單獨錄制音頻。它們處理的音頻文件是壓縮過的編碼文件,通常用於錄制和播放音樂,是最經常用到的。MediaRecorder與MediaPlayer在處理音頻和視頻時,整體流程是一樣的,只有在部分方法的調用上有所差異,下面分別把錄音/播音有關的方法列出來。

MediaRecorder的錄音相關方法:
reset : 重置錄制資源
prepare : 准備錄制
start : 開始錄制
stop : 結束錄制
release : 釋放錄制資源
setOnErrorListener : 設置錯誤監聽器。可監聽服務器異常以及未知錯誤的事件。
setOnInfoListener : 設置信息監聽器。可監聽錄制結束事件,包括達到錄制時長或者達到錄制大小。
setAudioSource : 設置音頻來源。一般使用麥克風AudioSource.MIC。
setOutputFormat : 設置媒體輸出格式。OutputFormat.AMR_NB表示窄帶格式,OutputFormat.AMR_WB表示寬帶格式,AAC_ADTS表示高級的音頻傳輸流格式。該方法要在setVideoEncoder之前調用,不然調用setAudioEncoder時會報錯“java.lang.IllegalStateException”。
setAudioEncoder : 設置音頻編碼器。AudioEncoder.AMR_NB表示窄帶編碼,AudioEncoder.AMR_WB表示寬帶編碼,AudioEncoder.AAC表示低復雜度的高級編碼,AudioEncoder.HE_AAC表示高效率的高級編碼,AudioEncoder.AAC_ELD表示增強型低延遲的高級編碼。
注意:setAudioEncoder應在setOutputFormat之後執行,否則會出現“setAudioEncoder called in an invalid state(2)”的異常。
setAudioSamplingRate : 設置音頻的采樣率,單位赫茲(Hz)。該方法為可選,AMRNB默認8khz,AMRWB默認16khz。
setAudioChannels : 設置音頻的聲道數。1表示單聲道,2表示雙聲道。該方法為可選
setAudioEncodingBitRate : 設置音頻每秒錄制的字節數。越大則音頻越清晰。該方法為可選
setMaxDuration : 設置錄制時長。單位毫秒。
setMaxFileSize : 設置錄制的媒體大小。單位字節。
setOutputFile : 設置輸出文件的路徑。


MediaPlayer的播音相關方法:
reset : 重置播放器
prepare : 准備播放
start : 開始播放
pause : 暫停播放
stop : 停止播放
setOnPreparedListener : 設置准備播放監聽器。
setOnCompletionListener : 設置結束播放監聽器。
setOnSeekCompleteListener : 設置播放拖動監聽器。
create : 創建指定Uri的播放器。
setDataSource : 設置播放數據來源。create與setDataSource只需設置其一。
setVolume : 設置音量。第一個參數是左聲道,第二個參數是右聲道,取值在0-1之間。
setAudioStreamType : 設置音頻流的類型。AudioManager.STREAM_MUSIC表示音樂,AudioManager.STREAM_RING表示鈴聲,AudioManager.STREAM_ALARM表示鬧鐘,AudioManager.STREAM_NOTIFICATION表示通知。
setLooping : 設置是否循環播放。
isPlaying : 判斷是否正在播放。
seekTo : 拖動播放進度到指定位置。
getCurrentPosition : 獲取當前播放進度所在的位置。
getDuration : 獲取播放時長。


下面是MediaRecorder與MediaPlayer組合處理音頻的示例代碼:
import java.io.File;

import com.example.exmaudio.util.Utils;

import android.app.Activity;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaRecorder.AudioEncoder;
import android.media.MediaRecorder.AudioSource;
import android.media.MediaRecorder.OnErrorListener;
import android.media.MediaRecorder.OnInfoListener;
import android.media.MediaRecorder;
import android.media.MediaRecorder.OutputFormat;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.util.Log;
import android.view.View.OnClickListener;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.TextView;

public class MediaRecordActivity extends Activity 
		implements OnClickListener, OnErrorListener, OnInfoListener {
	private static final String TAG = "MediaRecordActivity";
    private TextView tv_record;
    private Button btn_start;
    private Button btn_stop;
    private MediaRecorder mMediaRecorder;

    private TextView tv_play;
    private Button btn_play;
    private Button btn_pause;
    private MediaPlayer mMediaPlayer;
    private int mPosition;
    private boolean bFirstPlay = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_media_record);

        tv_record = (TextView) findViewById(R.id.tv_record);
        btn_start = (Button) findViewById(R.id.btn_start);
        btn_stop = (Button) findViewById(R.id.btn_stop);
        tv_play = (TextView) this.findViewById(R.id.tv_play);
        btn_play = (Button) findViewById(R.id.btn_play);
        btn_pause = (Button) findViewById(R.id.btn_pause);

        btn_start.setOnClickListener(this);
        btn_stop.setOnClickListener(this);
        btn_play.setOnClickListener(this);
        btn_pause.setOnClickListener(this);
        btn_start.setEnabled(true);
        btn_stop.setEnabled(false);
        btn_play.setEnabled(false);
        btn_pause.setEnabled(false);
        initPlay();
    }
    
	private void initPlay() {
        mMediaPlayer = new MediaPlayer();
        mMediaPlayer.setOnCompletionListener(new OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mp) {
        		btn_play.setEnabled(true);
        		btn_pause.setEnabled(false);
        		bFirstPlay = true;
        		mHandler.removeCallbacks(mPlayRun);
        		mPlayTime = 0;
            }
        });
	}

	private void preplay() {
		try {
			mMediaPlayer.reset();
			//mMediaPlayer.setVolume(0.5f, 0.5f);  //設置音量,可選
			mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
			String path = mRecordFile.getAbsolutePath();
			mMediaPlayer.setDataSource(path);
			Log.d(TAG, "audio path = "+path);
			mMediaPlayer.prepare();
		} catch (Exception e) {
			Log.d(TAG, "mMediaPlayer.prepare error: "+e.getMessage());
		}
		mPlayTime = 0;
	}
    
    private void startPlay() {
		try {
			if (bFirstPlay == true) {
				preplay();
				bFirstPlay = false;
			}
			mMediaPlayer.start();
		} catch (Exception e) {
			Log.d(TAG, "mMediaPlayer.start error: " + e.getMessage());
		}
		btn_play.setEnabled(false);
		btn_pause.setEnabled(true);
		mHandler.post(mPlayRun);
    }
    
	@Override
	protected void onPause() {
		// 先判斷是否正在播放
		if (mMediaPlayer.isPlaying()) {
			// 如果正在播放我們就先保存這個播放位置
			mPosition = mMediaPlayer.getCurrentPosition();
			mMediaPlayer.stop();
			mHandler.removeCallbacks(mPlayRun);
		}
		super.onPause();
	}

	@Override
	protected void onResume() {
		if (mMediaPlayer!=null && mPosition>0) {
			mMediaPlayer.seekTo(mPosition);
			mMediaPlayer.start();
			mHandler.post(mPlayRun);
		}
		super.onResume();
	}
	
	private void startRecord() {
		createRecordDir();
        mMediaRecorder = new MediaRecorder();
        mMediaRecorder.reset();
        mMediaRecorder.setOnErrorListener(this);
        mMediaRecorder.setOnInfoListener(this);
        mMediaRecorder.setAudioSource(AudioSource.MIC);  //音頻源
        mMediaRecorder.setOutputFormat(OutputFormat.AMR_NB);
        mMediaRecorder.setAudioEncoder(AudioEncoder.AMR_NB);  //音頻格式
        //mMediaRecorder.setAudioSamplingRate(8);  //音頻的采樣率。可選
        //mMediaRecorder.setAudioChannels(2);  //音頻的聲道數。可選
        //mMediaRecorder.setAudioEncodingBitRate(1024);  //音頻每秒錄制的字節數。可選
        mMediaRecorder.setMaxDuration(10 * 1000);  //設置錄制時長
        //mMediaRecorder.setMaxFileSize(1024*1024*10);  //setMaxFileSize與setMaxDuration設置其一即可
        mMediaRecorder.setOutputFile(mRecordFile.getAbsolutePath());
        try {
            mMediaRecorder.prepare();
            mMediaRecorder.start();
        } catch (Exception e) {
			Log.d(TAG, "mMediaRecorder.start error: " + e.getMessage());
        }
		btn_start.setEnabled(false);
		btn_stop.setEnabled(true);
		mRecordTime = 0;
		mHandler.post(mRecordRun);
    }
    
	private File mRecordFile = null;
	private void createRecordDir() {
		File sampleDir = new File(Environment.getExternalStorageDirectory()
				+ File.separator + "Download" + File.separator);
		if (!sampleDir.exists()) {
			sampleDir.mkdirs();
		}
		File recordDir = sampleDir;
		try {
			mRecordFile = File.createTempFile(Utils.getNowDateTime(), ".amr", recordDir);
			Log.d(TAG, mRecordFile.getAbsolutePath());
		} catch (Exception e) {
			Log.d(TAG, "createTempFile error: " + e.getMessage());
		}
	}

	private void stopRecord() {
		if (mMediaRecorder != null) {
			mMediaRecorder.setOnErrorListener(null);
			mMediaRecorder.setPreviewDisplay(null);
			try {
				mMediaRecorder.stop();
			} catch (Exception e) {
				Log.d(TAG, "mMediaRecorder.stop error: " + e.getMessage());
			}
			mMediaRecorder.release();
			mMediaRecorder = null;
		}
		btn_start.setEnabled(true);
		btn_stop.setEnabled(false);
		btn_play.setEnabled(true);
		mHandler.removeCallbacks(mRecordRun);
	}

	@Override
	public void onClick(View v) {
		int resid = v.getId();
		if (resid == R.id.btn_start) {
			startRecord();
		} else if (resid == R.id.btn_stop) {
			stopRecord();
		} else if (resid == R.id.btn_play) {
			startPlay();
		} else if (resid == R.id.btn_pause) {
			mMediaPlayer.pause();
			btn_play.setEnabled(true);
			btn_pause.setEnabled(false);
			mHandler.removeCallbacks(mPlayRun);
		}
	}
	
	private Handler mHandler = new Handler();
	
	private int mRecordTime = 0;
	private Runnable mRecordRun = new Runnable() {
		@Override
		public void run() {
			tv_record.setText(mRecordTime+"s");
			mRecordTime++;
			mHandler.postDelayed(this, 1000);
		}
	};

	private int mPlayTime = 0;
	private Runnable mPlayRun = new Runnable() {
		@Override
		public void run() {
			tv_play.setText(mPlayTime+"s");
			mPlayTime++;
			mHandler.postDelayed(this, 1000);
		}
	};
	
    @Override
    public void onError(MediaRecorder mr, int what, int extra) {
        if (mr != null) {
            mr.reset();
        }
    }

	@Override
	public void onInfo(MediaRecorder mr, int what, int extra) {
		if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED
				|| what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
			stopRecord();
		}
	}

}


AudioRecord/AudioTrack

話說Android搞出這麼多種錄音/播音方式,到底有什麼用處呢?其實這還是跟不同的需求和用途有關,譬如說語音通話,要求實時傳輸,手機這邊說一句話,那邊廂就同步聽到一句話。如果是MediaRecorder與MediaPlayer組合,只能整句話都錄完編碼好了,才能傳給對方去播放,這個實效性就太差了。於是適用於音頻實時處理的AudioRecord與AudioTrack組合就應運而生,該組合的音頻為原始的二進制音頻數據,沒有文件頭和文件尾,故而可以實現邊錄邊播的實時語音。


MediaRecorder錄制的音頻格式有amr、aac等,MediaPlayer支持播放的音頻格式除了amr、aac之外,還支持常見的mp3、wav、mid、ogg等經過壓縮編碼的音頻。AudioRecord錄制的音頻格式只有pcm,AudioTrack可直接播放的也只有pcm。pcm格式有個缺點,在播放過程中不能直接暫停,因為二進制流;但pcm格式有個好處,就是iOS不能播放amr音頻,但能播放pcm音頻;所以如果Android手機錄制的音樂需要傳給iOS手機播放,還是得采用pcm格式。


下面是AudioRecord與AudioTrack組合的錄音/播音相關說明。


AudioRecord的錄音相關方法:
getMinBufferSize : 根據采樣頻率、聲道配置、音頻格式獲得合適的緩沖區大小。該函數為靜態方法。
構造函數 : 可設置錄音來源、采樣頻率、聲道配置、音頻格式與緩沖區大小。其中錄音來源一般是AudioSource.MIC,采樣頻率可取值8000或者16000,聲道配置可取值AudioFormat.CHANNEL_IN_STEREO或者AudioFormat.CHANNEL_OUT_STEREO,音頻格式可取值AudioFormat.ENCODING_PCM_16BIT或者AudioFormat.ENCODING_PCM_8BIT。
startRecording : 開始錄音。
read : 從緩沖區中讀取音頻數據,此數據用於保存到音頻文件中。
stop : 停止錄音。
release : 停止錄音並釋放資源。
setNotificationMarkerPosition : 設置需要通知的標記位置。
setPositionNotificationPeriod : 設置需要通知的時間周期。
setRecordPositionUpdateListener : 設置錄制位置變化的監聽器對象。該監聽器從OnRecordPositionUpdateListener擴展而來,需要實現onMarkerReached和onPeriodicNotification兩個方法;其中onMarkerReached事件的觸發對應於setNotificationMarkerPosition方法,onPeriodicNotification事件的觸發對應於setPositionNotificationPeriod方法。


AudioTrack的播音相關方法:
getMinBufferSize : 根據采樣頻率、聲道配置、音頻格式獲得合適的緩沖區大小。該函數為靜態方法。
構造函數 : 可設置音頻類型、采樣頻率、聲道配置、音頻格式、播放模式與緩沖區大小。其中音頻類型一般是AudioManager.STREAM_MUSIC,采樣頻率、聲道配置、音頻格式與錄音時保持一致,播放模式一般是AudioTrack.MODE_STREAM。
setStereoVolume : 設置立體聲的音量。第一個參數是左聲道音量,第二個參數是右聲道音量。
play : 開始播放。
write : 把緩沖區的音頻數據寫入音軌中。調用該函數前要先從音頻文件中讀取數據寫入緩沖區。
stop : 停止播放。
release : 停止播放並釋放資源。
setNotificationMarkerPosition : 設置需要通知的標記位置。
setPositionNotificationPeriod : 設置需要通知的時間周期。
setPlaybackPositionUpdateListener : 設置播放位置變化的監聽器對象。該監聽器從OnPlaybackPositionUpdateListener擴展而來,需要實現onMarkerReached和onPeriodicNotification兩個方法;其中onMarkerReached事件的觸發對應於setNotificationMarkerPosition方法,onPeriodicNotification事件的觸發對應於setPositionNotificationPeriod方法。


下面是AudioRecord與AudioTrack組合處理音頻的示例代碼:
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

import com.example.exmaudio.util.Utils;

import android.app.Activity;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioRecord.OnRecordPositionUpdateListener;
import android.media.AudioTrack.OnPlaybackPositionUpdateListener;
import android.media.AudioTrack;
import android.media.MediaRecorder.AudioSource;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

public class AudioRecordActivity extends Activity implements OnClickListener {
	private static final String TAG = "AudioRecordActivity";

	private TextView tv_record, tv_play;
	private Button btn_start, btn_stop, btn_play, btn_finish;
	private boolean isRecording, isPlaying;
	private Handler mHandler = new Handler();
	private int mRecordTime, mPlayTime;

	private int frequence = 8000;
	private int channelConfig = AudioFormat.CHANNEL_IN_STEREO; //只能取值CHANNEL_OUT_STEREO
	//如果取值CHANNEL_OUT_DEFAULT,會報錯“getMinBufferSize(): Invalid channel configuration.”
	//如果取值CHANNEL_OUT_MONO,會報錯“java.lang.IllegalArgumentException: Unsupported channel configuration.”
	private int audioFormat = AudioFormat.ENCODING_PCM_16BIT; //AudioRecord只能錄制PCM格式

	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_audio_record);

		tv_record = (TextView) findViewById(R.id.tv_record);
		btn_start = (Button) findViewById(R.id.btn_start);
		btn_stop = (Button) findViewById(R.id.btn_stop);
		tv_play = (TextView) findViewById(R.id.tv_play);
		btn_play = (Button) findViewById(R.id.btn_play);
		btn_finish = (Button) findViewById(R.id.btn_finish);

		btn_start.setEnabled(true);
		btn_stop.setEnabled(false);
		btn_play.setEnabled(false);
		btn_finish.setEnabled(false);
		btn_start.setOnClickListener(this);
		btn_stop.setOnClickListener(this);
		btn_play.setOnClickListener(this);
		btn_finish.setOnClickListener(this);
		createRecordDir();
	}

    private File mRecordFile = null;
	private void createRecordDir() {
		File sampleDir = new File(Environment.getExternalStorageDirectory()
				+ File.separator + "Download" + File.separator);
		if (!sampleDir.exists()) {
			sampleDir.mkdirs();
		}
		File recordDir = sampleDir;
		try {
			mRecordFile = File.createTempFile(Utils.getNowDateTime(), ".pcm", recordDir);
			Log.d(TAG, mRecordFile.getAbsolutePath());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public void onClick(View v) {
		int resid = v.getId();
		if (resid == R.id.btn_start) {
			isRecording = true;
			new RecordTask().execute();
		} else if (resid == R.id.btn_stop) {
			isRecording = false;
		} else if (resid == R.id.btn_play) {
			isPlaying = true;
			new PlayTask().execute();
		} else if (resid == R.id.btn_finish) {
			isPlaying = false;
		}
	}

	private void refreshStatus(boolean isRecord, boolean isPlay) {
		if (isRecord || isPlay) {
			btn_start.setEnabled(false);
			btn_stop.setEnabled(isRecord?true:false);
			btn_play.setEnabled(false);
			btn_finish.setEnabled(isPlay?true:false);
		} else {
			btn_start.setEnabled(true);
			btn_stop.setEnabled(false);
			btn_play.setEnabled(true);
			btn_finish.setEnabled(false);
		}
	}

	private class RecordTask extends AsyncTask {
		@Override
		protected Void doInBackground(Void... arg0) {
			try {
				// 開通輸出流到指定的文件
				DataOutputStream dos = new DataOutputStream(
						new BufferedOutputStream(new FileOutputStream(mRecordFile)));
				// 根據定義好的幾個配置,來獲取合適的緩沖大小
				int bsize = AudioRecord.getMinBufferSize(frequence, channelConfig, audioFormat);
				AudioRecord record = new AudioRecord(AudioSource.MIC, 
						frequence, channelConfig, audioFormat, bsize);
				// 定義緩沖區
				short[] buffer = new short[bsize];
				//record.setNotificationMarkerPosition(1000);
				record.setPositionNotificationPeriod(1000);
				record.setRecordPositionUpdateListener(new RecordUpdateListener());
				record.startRecording();

				while (isRecording) {
					int bufferReadResult = record.read(buffer, 0, buffer.length);
					// 循環將buffer中的音頻數據寫入到OutputStream中
					for (int i = 0; i < bufferReadResult; i++) {
						dos.writeShort(buffer[i]);
					}
				}
				record.stop();
				dos.close();
				Log.d(TAG, "mRecordFile.length()=" + mRecordFile.length());
			} catch (Exception e) {
				e.printStackTrace();
			}
			return null;
		}

		@Override
		protected void onPreExecute() {
			refreshStatus(true, false);
			mRecordTime = 0;
			mHandler.postDelayed(mRecordRun, 1000);
		}

		@Override
		protected void onPostExecute(Void result) {
			refreshStatus(false, false);
			mHandler.removeCallbacks(mRecordRun);
		}

	}
	
	private Runnable mRecordRun = new Runnable() {
		@Override
		public void run() {
			mRecordTime++;
			mHandler.postDelayed(this, 1000);
		}
	};
	
	private class RecordUpdateListener implements OnRecordPositionUpdateListener {

		@Override
		public void onMarkerReached(AudioRecord recorder) {
		}

		@Override
		public void onPeriodicNotification(AudioRecord recorder) {
			tv_record.setText(mRecordTime+"s");
		}
		
	}
	
	private class PlayTask extends AsyncTask {
		@Override
		protected Void doInBackground(Void... arg0) {
			try {
				// 定義輸入流,將音頻寫入到AudioTrack類中,實現播放
				DataInputStream dis = new DataInputStream(
						new BufferedInputStream(new FileInputStream(mRecordFile)));
				int bsize = AudioTrack.getMinBufferSize(frequence, channelConfig, audioFormat);
				short[] buffer = new short[bsize / 4];
				AudioTrack track = new AudioTrack(AudioManager.STREAM_MUSIC,
						frequence, channelConfig, audioFormat, bsize, AudioTrack.MODE_STREAM);
				//track.setNotificationMarkerPosition(1000);
				track.setPositionNotificationPeriod(1000);
				track.setPlaybackPositionUpdateListener(new PlaybackUpdateListener());
				track.play();
				// 由於AudioTrack播放的是流,所以,我們需要一邊播放一邊讀取
				while (isPlaying && dis.available() > 0) {
					int i = 0;
					while (dis.available() > 0 && i < buffer.length) {
						buffer[i] = dis.readShort();
						i++;
					}
					// 然後將數據寫入到AudioTrack中
					track.write(buffer, 0, buffer.length);
				}
				track.stop();
				dis.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
			return null;
		}

		@Override
		protected void onPreExecute() {
			refreshStatus(false, true);
			mPlayTime = 0;
			mHandler.postDelayed(mPlayRun, 1000);
		}

		@Override
		protected void onPostExecute(Void result) {
			refreshStatus(false, false);
			mHandler.removeCallbacks(mPlayRun);
		}

	}

	private Runnable mPlayRun = new Runnable() {
		@Override
		public void run() {
			mPlayTime++;
			mHandler.postDelayed(this, 1000);
		}
	};
	
	private class PlaybackUpdateListener implements OnPlaybackPositionUpdateListener {

		@Override
		public void onMarkerReached(AudioTrack track) {
		}

		@Override
		public void onPeriodicNotification(AudioTrack track) {
			tv_play.setText(mPlayTime+"s");
		}
		
	}
	
}


SoundPool

App使用過程中經常有些短小的提示聲音,比如拍照的咔嚓聲、掃一掃的吡一聲,還有玩游戲擊中目標的嗒嗒聲,這些片段聲音基本是系統自帶的。如果使用MediaPlayer來播放,便存在諸如下面的不足之處:資源占用量較高、延遲時間較長、不支持多個音頻同時播放等等。因此,我們需要一個短聲音專用的播放器,這個播放器在Android中就是SoundPool。


SoundPool在使用時可以事先加載多個音頻,然後在需要的時候播放指定編號的音頻,這樣處理有幾個好處:
1、資源占用量小,不像MediaPlayer那麼重;
2、延遲時間相對MediaPlayer延遲非常小;
3、可以同時播放多個音頻,從而實現游戲過程中多個有效疊加的情景;
當然,SoundPool帶來方便的同時也做了一部分犧牲,下面是使用它的一些限制:
1、SoundPool最大只能申請1M的內存,這意味著它只能播放一些很短的聲音片段,不能用於播放歌曲或者游戲背景音樂;
2、雖然SoundPool提供了pause和stop方法,但是輕易不要使用這兩個方法,因為它們可能會讓你的App異常或崩潰;
3、SoundPool播放的音頻格式建議使用ogg格式,據說它對wav格式的支持不太好;
4、待播放的音頻要提前加載進SoundPool,不要等到要播放的時候才加載。因為SoundPool不會等音頻加載完了才播放,所以它的延遲才比較小;而MediaPlayer會等待加載完畢才播放,所以延遲會比較大。


下面是SoundPool的常用方法說明:
構造函數 : 可設置最大個數、音頻類型、音頻質量。其中音頻類型一般是AudioManager.STREAM_MUSIC,質量取值為0到100。
load : 加載指定的音頻,該音頻可以是個磁盤文件,也可以是資源文件。返回值為該音頻的編號。
unload : 卸載指定編號的音頻。
play : 播放指定編號的音頻。可同時設置左右聲道的音量(取值為0.0到1.0)、優先級(0為最低)、是否循環播放(0為只播放一次,-1為無限循環)、播放速率(取值為0.5-2.0,其中1.0為正常速率)。
setVolume : 設置指定編號音頻的音量大小。
setPriority : 設置指定編號音頻的優先級。
setLoop : 設置指定編號的音頻是否循環播放。
setRate : 設置指定編號音頻的播放速率。
pause : 暫停播放指定編號的音頻。
resume : 恢復播放指定編號的音頻。
autoPause : 暫停所有正在播放的音頻。
autoResume : 恢復播放所有被暫停的音頻。
stop : 停止播放指定編號的音頻。
release : 釋放所有音頻資源。
setOnLoadCompleteListener : 設置音頻加載完畢的監聽器。該監聽器擴展自OnLoadCompleteListener,需要重寫onLoadComplete方法。


下面是SoundPool播放音頻的示例代碼:
import java.util.HashMap;

import android.app.Activity;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class SoundPlayActivity extends Activity implements OnClickListener {

	private SoundPool mSoundPool;
	private HashMap mSoundMap;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_sound_play);

		Button btn_play_all = (Button) findViewById(R.id.btn_play_all);
		Button btn_play_first = (Button) findViewById(R.id.btn_play_first);
		Button btn_play_second = (Button) findViewById(R.id.btn_play_second);
		Button btn_play_third = (Button) findViewById(R.id.btn_play_third);
		btn_play_all.setOnClickListener(this);
		btn_play_first.setOnClickListener(this);
		btn_play_second.setOnClickListener(this);
		btn_play_third.setOnClickListener(this);
		
		mSoundMap = new HashMap();
		mSoundPool = new SoundPool(3, AudioManager.STREAM_MUSIC, 100);
		loadSound(1, R.raw.beep1);
		loadSound(2, R.raw.beep2);
		loadSound(3, R.raw.ring);
	}
	
	private void loadSound(int seq, int resid) {
		int soundID = mSoundPool.load(this, resid, 1);
		mSoundMap.put(seq, soundID);
	}

	private void playSound(int seq) {
		int soundID = mSoundMap.get(seq);
		mSoundPool.play(soundID, 1.0f, 1.0f, 1, 0, 1.0f);
	}

	@Override
	public void onClick(View v) {
		if (v.getId() == R.id.btn_play_all) {
			playSound(1);
			playSound(2);
			playSound(3);
		} else if (v.getId() == R.id.btn_play_first) {
			playSound(1);
		} else if (v.getId() == R.id.btn_play_second) {
			playSound(2);
		} else if (v.getId() == R.id.btn_play_third) {
			playSound(3);
		}
	}
	
	@Override
	protected void onDestroy() {
		if (mSoundPool != null) {
			mSoundPool.release();
		}
		super.onDestroy();
	}

}


自定義音樂播放器

大家常見的音樂播放器,不外乎主要有三項功能:
1、展示音樂/歌曲列表;
2、滾動展示歌詞,並高亮顯示當前正在播放的詞句;
3、展示控制欄顯示播放進度,並提供開始/暫停、拖動播放的功能,以及同時控制歌詞的滾動情況;


對於第一點的展示歌曲列表,通過手工添加很費時費力,而且用戶往往搞不清楚手機上的歌曲都放在哪個目錄。我們假設用戶是傻白甜,那自己開發的App就得智能貼心,主動幫用戶把手機上的歌曲找出來。要實現這個功能,就到系統自帶的媒體庫中去查找,媒體庫裡音頻資源的詳細路徑是MediaStore.Audio.Media.EXTERNAL_CONTENT_URI這個Uri,訪問裡面的音頻記錄,可以通過ContentResolver來完成。有關ContentResolver的具體用法參見《Android開發筆記(五十四)數據共享接口ContentProvider》。下面是MediaStore.Audio.Media.EXTERNAL_CONTENT_URI裡的主要字段信息說明:
Audio.Media._ID : 歌曲編號。
Audio.Media.TITLE : 歌曲的標題名稱。
Audio.Media.ALBUM : 歌曲的專輯名稱。
Audio.Media.DURATION : 歌曲的播放時間。
Audio.Media.SIZE : 歌曲文件的賭大小。
Audio.Media.ARTIST : 歌曲的演唱者。
Audio.Media.DATA : 歌曲文件的完整路徑。


對於第二點的滾動歌詞顯示,通用的歌詞文件是lrc格式的文本文件,內容主要是每句歌詞的文字與開始時間。文本文件的解析並不復雜,難點主要在滾動顯示上面。乍看起來歌詞從下往上滾動,采用平移動畫TranslateAnimation正合適;可是歌詞滾動可不是勻速的,因為每句歌詞的間隔時間並不固定,只能把整個歌詞滾動分解為若干個動畫,每個平移動畫只負責前後兩行歌詞之間的滾動效果,前一行歌詞的平移動畫滾動完畢,馬上開始下一行歌詞的平移動畫。另外,高亮顯示當前演奏的歌詞,這等於一段文字內的部分文字風格改變,雖然可以讓每行文字都用單獨的TextView來展示,但是一堆的TextView控件同時滾動很影響UI性能,所以建議采用可變字符串SpannableString直接處理段內文字,它的具體說明參見《Android開發筆記(六)可變字符串》。


對於第三點的歌曲控制欄,總體上復用前一篇博文提到的視頻控制欄VideoController,博文名稱是《Android開發筆記(一百二十五)自定義視頻播放器》。不過歌曲控制欄還要更復雜,因為除了控制音頻的播放,還要控制歌詞動畫的播放。更要命的是,平移動畫TranslateAnimation居然不支持暫停和恢復操作,而且不只是平移動畫,所有補間動畫都不支持暫停和恢復。難道又要自己重定義動畫了嗎?剛想到這個的時候,不要說讀者,就連筆者自己都想撞牆了。山窮水盡疑無路,柳暗花明又一村,幸好Android還給我們提供了屬性動畫這麼一個好東東,屬性動畫不但支持所有的補間動畫效果,而且也支持暫停和恢復操作,所以還等什麼,趕緊把TranslateAnimation換成了ObjectAnimator。有關屬性動畫的詳細介紹參見《Android開發筆記(九十六)集合動畫與屬性動畫》。


弄完以上三點功能,一個主流音樂播放器的雛形便出來了,下面是音樂播放器的歌曲列表截圖:
\


下面是音樂播放器的歌曲詳情頁的效果截圖:
\\


下面是音樂播放器的歌曲詳情頁面的代碼例子:
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.util.ArrayList;

import com.example.exmaudio.bean.LrcContent;
import com.example.exmaudio.bean.MusicInfo;
import com.example.exmaudio.util.LyricsLoader;
import com.example.exmaudio.util.Utils;
import com.example.exmaudio.widget.AudioController;
import com.example.exmaudio.widget.AudioController.onSeekChangeListener;

import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.app.Activity;
import android.graphics.Color;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.view.View;
import android.view.animation.AnimationUtils;
import android.widget.TextView;

@TargetApi(Build.VERSION_CODES.KITKAT)
public class MusicDetailActivity extends Activity 
		implements AnimatorListener, onSeekChangeListener {
	private static final String TAG = "MusicDetailActivity";

	private TextView tv_title;
	private TextView tv_artist;
	private TextView tv_music;
	private MusicInfo mMusic;
    private MediaPlayer mMediaPlayer;
	private AudioController ac_play;
	private LyricsLoader mLoader;
	private ArrayList mLrcList;
	private Handler mHandler = new Handler();

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_music_detail);

		tv_title = (TextView) findViewById(R.id.tv_title);
		tv_artist = (TextView) findViewById(R.id.tv_artist);
		tv_music = (TextView) findViewById(R.id.tv_music);
		ac_play = (AudioController) findViewById(R.id.ac_play);
		
		ac_play.setonSeekChangeListener(this);
		mMusic = getIntent().getParcelableExtra("music");
		tv_title.setText(mMusic.getTitle());
		tv_artist.setText(mMusic.getArtist());
		mLoader = LyricsLoader.getInstance(mMusic.getUrl());
		mLrcList = mLoader.getLrcList();
        mMediaPlayer = new MediaPlayer();
        playMusic(mMusic.getUrl());
	}

    private void playMusic(String file_path) {
		if (mMediaPlayer.isPlaying()) {
			mMediaPlayer.stop();
		}
		if (Utils.getExtendName(file_path).equals("pcm")) {
			ac_play.setVisibility(View.GONE);
			PlayTask playTask = new PlayTask();
			playTask.execute(file_path);
		} else {
			playMedia(file_path);
		}
    }

	private void playMedia(String filePath) {
		try {
			mMediaPlayer.reset();
			//mMediaPlayer.setVolume(0.5f, 0.5f);  //設置音量,可選
			mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
			mMediaPlayer.setDataSource(filePath);
			mMediaPlayer.prepare();
			mMediaPlayer.start();
			mHandler.post(mRefreshCtrl);
			mMediaPlayer.setOnCompletionListener(new OnCompletionListener() {
				@Override
				public void onCompletion(MediaPlayer mp) {
					ac_play.setCurrentTime(0, 0);
				}
			});
			ac_play.setMediaPlayer(mMediaPlayer);

			//以下處理歌詞
			if (mLoader.getLrcList()!=null && mLrcList.size()>0) {
				mLrcStr = "";
				for (int i=0; i seekto) {
				break;
			}
		}
		mCount = i;
		mPrePos = -1;
		mNextPos = 0;
		if (mCount > 0) {
			for (int j = 0; j < mCount; j++) {
				mNextPos = mLrcStr.indexOf(" \n",="" mprepos="" 1);="" mnextpos);="" startanimation(-mlineheight*i,="" onmusicpause()="" animtrany.pause();="" onmusicresume()="" animtrany.resume();="" mcount="0;" float="" mcurrentheight="0;" mlineheight="0;" mrefreshlrc="new" (mlineheight="=" 0)="" (tv_music.getheight()-tv_music.getpaddingtop())="" mlrclist.size()="" 2;="" "tv_music.getheight()="+tv_music.getHeight());
				Log.d(TAG, " tv_music.getpaddingtop()="+tv_music.getPaddingTop());
				Log.d(TAG, " ,mcurrentheight="+mCurrentHeight+" ,getheight="+tv_music.getHeight());
		}
	};

	private int mPrePos = -1, mNextPos = 0;
	private String mLrcStr;
	private ObjectAnimator animTranY;

	public void startAnimation(float aimHeight, int offset) {
		Log.d(TAG, " aimheight="+aimHeight);
		animTranY = ObjectAnimator.ofFloat(tv_music, " translationy",="" mcurrentheight,="" aimheight);="" animtrany.setduration(offset);="" animtrany.setrepeatcount(0);="" animtrany.addlistener(this);="" animtrany.start();="" onanimationstart(animator="" animation)="" onanimationend(animator="" (mcount="" 0?mNextPos:mLrcStr.length()-1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
			mCount++;
			tv_music.setText(spanText);
			if (mNextPos > 0 && mNextPos < mLrcStr.length()-1) {
				mPrePos = mLrcStr.indexOf("\n", mNextPos);
				mHandler.postDelayed(mRefreshLrc, 50);
			}
		}
	}

	@Override
	public void onAnimationCancel(Animator animation) {
	}

	@Override
	public void onAnimationRepeat(Animator animation) {
	}
	
	private int frequence = 8000;
	private int channelConfig = AudioFormat.CHANNEL_IN_STEREO; //只能取值CHANNEL_OUT_STEREO
	private int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
	private class PlayTask extends AsyncTask {
		@Override
		protected Void doInBackground(String... arg0) {
			try {
				// 定義輸入流,將音頻寫入到AudioTrack類中,實現播放
				DataInputStream dis = new DataInputStream(
						new BufferedInputStream(new FileInputStream(arg0[0])));
				int bsize = AudioTrack.getMinBufferSize(frequence, channelConfig, audioFormat);
				short[] buffer = new short[bsize / 4];
				AudioTrack track = new AudioTrack(AudioManager.STREAM_MUSIC,
						frequence, channelConfig, audioFormat, bsize, AudioTrack.MODE_STREAM);
				track.play();
				// 由於AudioTrack播放的是流,所以,我們需要一邊播放一邊讀取
				while (dis.available() > 0) {
					int i = 0;
					while (dis.available() > 0 && i < buffer.length) {
						buffer[i] = dis.readShort();
						i++;
					}
					// 然後將數據寫入到AudioTrack中
					track.write(buffer, 0, buffer.length);
				}
				track.stop();
				dis.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
			return null;
		}

	}

}


下面是音樂播放器的歌曲控制欄的代碼例子:
import com.example.exmaudio.R;
import com.example.exmaudio.util.Utils;

import android.content.Context;
import android.graphics.Color;
import android.media.MediaPlayer;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;

public class AudioController extends RelativeLayout implements OnClickListener, OnSeekBarChangeListener {
	private static final String TAG = "AudioController";

	private Context mContext;
	private ImageView mImagePlay;
	private TextView mCurrentTime;
	private TextView mTotalTime;
	private SeekBar mSeekBar;
	private int mBeginViewId = 0x7F24FFF0;
	private int dip_10, dip_40;

	private MediaPlayer mMediaPlayer;
	private int mCurrent = 0;
	private int mBuffer = 0;
	private int mDuration = 0;
	private boolean bPause = false;
	
	public AudioController(Context context) {
		this(context, null);
	}

	public AudioController(Context context, AttributeSet attrs) {
		super(context, attrs);
		mContext = context;
		dip_10 = Utils.dip2px(mContext, 10);
		dip_40 = Utils.dip2px(mContext, 40);
		initView();
	}

	private TextView newTextView(Context context, int id) {
		TextView tv = new TextView(context);
		tv.setId(id);
		tv.setGravity(Gravity.CENTER);
		tv.setTextColor(Color.WHITE);
		tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
		RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
				LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
		params.addRule(RelativeLayout.CENTER_VERTICAL);
		tv.setLayoutParams(params);
		return tv;
	}
	
	private void initView() {
		mImagePlay = new ImageView(mContext);
		RelativeLayout.LayoutParams imageParams = new RelativeLayout.LayoutParams(dip_40, dip_40);
		imageParams.addRule(RelativeLayout.CENTER_VERTICAL);
		mImagePlay.setLayoutParams(imageParams);
		mImagePlay.setId(mBeginViewId);
		mImagePlay.setOnClickListener(this);
		
		mCurrentTime = newTextView(mContext, mBeginViewId+1);
		RelativeLayout.LayoutParams currentParams = (LayoutParams) mCurrentTime.getLayoutParams();
		currentParams.setMargins(dip_10, 0, 0, 0);
		currentParams.addRule(RelativeLayout.RIGHT_OF, mImagePlay.getId());
		mCurrentTime.setLayoutParams(currentParams);

		mTotalTime = newTextView(mContext, mBeginViewId+2);
		RelativeLayout.LayoutParams totalParams = (LayoutParams) mTotalTime.getLayoutParams();
		totalParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
		mTotalTime.setLayoutParams(totalParams);
		
		mSeekBar = new SeekBar(mContext);
		RelativeLayout.LayoutParams seekParams = new RelativeLayout.LayoutParams(
				LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
		totalParams.setMargins(dip_10, 0, dip_10, 0);
		seekParams.addRule(RelativeLayout.CENTER_IN_PARENT);
		seekParams.addRule(RelativeLayout.RIGHT_OF, mCurrentTime.getId());
		seekParams.addRule(RelativeLayout.LEFT_OF, mTotalTime.getId());
		mSeekBar.setLayoutParams(seekParams);
		mSeekBar.setMax(100);
		mSeekBar.setMinimumHeight(100);
		mSeekBar.setThumbOffset(0);
		mSeekBar.setId(mBeginViewId+3);
		mSeekBar.setOnSeekBarChangeListener(this);
	}

	private void reset() {
		if (mCurrent == 0 || bPause) {
			mImagePlay.setImageResource(R.drawable.audio_btn_down);
		} else {
			mImagePlay.setImageResource(R.drawable.audio_btn_on);
		}
		mCurrentTime.setText(Utils.formatTime(mCurrent));
		mTotalTime.setText(Utils.formatTime(mDuration));
		mSeekBar.setProgress((mCurrent==0)?0:(mCurrent*100/mDuration));
		mSeekBar.setSecondaryProgress(mBuffer);
	}
	
	private void refresh() {
		invalidate();
		requestLayout();
	}
	
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		super.onLayout(changed, l, t, r, b);
		removeAllViews();
		reset();
		addView(mImagePlay);
		addView(mCurrentTime);
		addView(mTotalTime);
		addView(mSeekBar);
	}

	@Override
	public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
		if (fromUser) {
			int time = progress * mDuration / 100;
			mMediaPlayer.seekTo(time);
		}
	}

	@Override
	public void onStartTrackingTouch(SeekBar seekBar) {
	}

	@Override
	public void onStopTrackingTouch(SeekBar seekBar) {
		int time = seekBar.getProgress() * mDuration / 100;
		mSeekListener.onMusicSeek(mMediaPlayer.getCurrentPosition(), time);
	}
	
	private onSeekChangeListener mSeekListener;
	public static interface onSeekChangeListener {
		public void onMusicSeek(int current, int seekto);
		public void onMusicPause();
		public void onMusicResume();
	}
	public void setonSeekChangeListener(onSeekChangeListener listener) {
		mSeekListener = listener;
	}

	@Override
	public void onClick(View v) {
		if (v.getId() == mImagePlay.getId()) {
			if (mMediaPlayer.isPlaying()) {
				mMediaPlayer.pause();
				bPause = true;
				mSeekListener.onMusicPause();
			} else {
				if (mCurrent == 0) {
					mSeekListener.onMusicSeek(0, 0);
				}
				mMediaPlayer.start();
				bPause = false;
				mSeekListener.onMusicResume();
			}
		}
		refresh();
	}
	
	public void setMediaPlayer(MediaPlayer view) {
		mMediaPlayer = view;
		mDuration = mMediaPlayer.getDuration();
	}
	
	public void setCurrentTime(int current_time, int buffer_time) {
		mCurrent = current_time;
		mBuffer = buffer_time;
		refresh();
	}

}
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved