多多色-多人伦交性欧美在线观看-多人伦精品一区二区三区视频-多色视频-免费黄色视屏网站-免费黄色在线

國內最全IT社區平臺 聯系我們 | 收藏本站
阿里云優惠2
您當前位置:首頁 > php開源 > 綜合技術 > Android開發筆記(一百二十六)自定義音樂播放器

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

來源:程序員人生   發布時間:2017-01-12 11:57:48 閱讀次數:5284次

MediaRecorder/MediaPlayer

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


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


MediaRecorder的錄音相干方法:
reset : 重置錄制資源
prepare : 準備錄制
start : 開始錄制
stop : 結束錄制
release : 釋放錄制資源
setOnErrorListener : 設置毛病監聽器。可監聽服務器異常和未知毛病的事件。
setOnInfoListener : 設置信息監聽器。可監聽錄制結束事件,包括到達錄制時長或到達錄制大小。
setAudioSource : 設置音頻來源。1般使用麥克風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只需設置其1。
setVolume : 設置音量。第1個參數是左聲道,第2個參數是右聲道,取值在0⑴之間。
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設置其1便可 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弄出這么多種錄音/播音方式,到底有甚么用途呢?其實這還是跟不同的需求和用處有關,比方說語音通話,要求實時傳輸,手機這邊說1句話,那邊廂就同步聽到1句話。如果是MediaRecorder與MediaPlayer組合,只能整句話都錄完編碼好了,才能傳給對方去播放,這個實效性就太差了。因而適用于音頻實時處理的AudioRecord與AudioTrack組合就應運而生,該組合的音頻為原始的2進制音頻數據,沒有文件頭和文件尾,故而可以實現邊錄邊播的實時語音。


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


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


AudioRecord的錄音相干方法:
getMinBufferSize : 根據采樣頻率、聲道配置、音頻格式取得適合的緩沖區大小。該函數為靜態方法。
構造函數 : 可設置錄音來源、采樣頻率、聲道配置、音頻格式與緩沖區大小。其中錄音來源1般是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 : 根據采樣頻率、聲道配置、音頻格式取得適合的緩沖區大小。該函數為靜態方法。
構造函數 : 可設置音頻類型、采樣頻率、聲道配置、音頻格式、播放模式與緩沖區大小。其中音頻類型1般是AudioManager.STREAM_MUSIC,采樣頻率、聲道配置、音頻格式與錄音時保持1致,播放模式1般是AudioTrack.MODE_STREAM。
setStereoVolume : 設置立體聲的音量。第1個參數是左聲道音量,第2個參數是右聲道音量。
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<Void, Integer, Void> { @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<Void, Integer, Void> { @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播放的是流,所以,我們需要1邊播放1邊讀取 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使用進程中常常有些短小的提示聲音,比如拍照的咔嚓聲、掃1掃的吡1聲,還有玩游戲擊中目標的噠噠聲,這些片斷聲音基本是系統自帶的。如果使用MediaPlayer來播放,便存在諸以下面的不足的地方:資源占用量較高、延遲時間較長、不支持多個音頻同時播放等等。因此,我們需要1個短聲音專用的播放器,這個播放器在Android中就是SoundPool。


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


下面是SoundPool的經常使用方法說明:
構造函數 : 可設置最大個數、音頻類型、音頻質量。其中音頻類型1般是AudioManager.STREAM_MUSIC,質量取值為0到100。
load : 加載指定的音頻,該音頻可以是個磁盤文件,也能夠是資源文件。返回值為該音頻的編號。
unload : 卸載指定編號的音頻。
play : 播放指定編號的音頻。可同時設置左右聲道的音量(取值為0.0到1.0)、優先級(0為最低)、是不是循環播放(0為只播放1次,⑴為無窮循環)、播放速率(取值為0.5⑵.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<Integer, Integer> 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<Integer, Integer>(); 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(); } }


自定義音樂播放器

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


對第1點的展現歌曲列表,通過手工添加很費時費力,而且用戶常常弄不清楚手機上的歌曲都放在哪一個目錄。我們假定用戶是傻白甜,那自己開發的App就得智能貼心,主動幫用戶把手機上的歌曲找出來。要實現這個功能,就到系統自帶的媒體庫中去查找,媒體庫里音頻資源的詳細路徑是MediaStore.Audio.Media.EXTERNAL_CONTENT_URI這個Uri,訪問里面的音頻記錄,可以通過ContentResolver來完成。有關ContentResolver的具體用法參見《Android開發筆記(5104)數據同享接口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 : 歌曲文件的完全路徑。


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


對第3點的歌曲控制欄,整體上復用前1篇博文提到的視頻控制欄VideoController,博文名稱是《Android開發筆記(1百2105)自定義視頻播放器》。不過歌曲控制欄還要更復雜,由于除控制音頻的播放,還要控制歌詞動畫的播放。更要命的是,平移動畫TranslateAnimation竟然不支持暫停和恢復操作,而且不只是平移動畫,所有補間動畫都不支持暫停和恢復。難道又要自己重定義動畫了嗎?剛想到這個的時候,不要說讀者,就連筆者自己都想撞墻了。窮途末路疑無路,柳暗花明又1村,幸虧Android還給我們提供了屬性動畫這么1個好東東,屬性動畫不但支持所有的補間動畫效果,而且也支持暫停和恢復操作,所以還等甚么,趕快把TranslateAnimation換成了ObjectAnimator。有關屬性動畫的詳細介紹參見《Android開發筆記(9106)集合動畫與屬性動畫》。


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



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


下面是音樂播放器的歌曲詳情頁面的代碼例子:
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<LrcContent> 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<mLrcList.size(); i++) { LrcContent item = mLrcList.get(i); mLrcStr = mLrcStr + item.getLrcStr() + "\n"; } tv_music.setText(mLrcStr); tv_music.setAnimation(AnimationUtils.loadAnimation(this,R.anim.alpha)); mHandler.postDelayed(mRefreshLrc, 100); } } catch (Exception e) { Log.d(TAG, "mMediaPlayer.prepare error: "+e.getMessage()); } } @Override protected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null); } private Runnable mRefreshCtrl = new Runnable() { @Override public void run() { if (mMediaPlayer.isPlaying()) { ac_play.setCurrentTime(mMediaPlayer.getCurrentPosition(), 0); } mHandler.postDelayed(this, 500); } }; @Override public void onMusicSeek(int current, int seekto) { Log.d(TAG, "current="+current+", seekto="+seekto); animTranY.cancel(); mHandler.removeCallbacks(mRefreshLrc); int i; for (i=0; i<mLrcList.size(); i++) { LrcContent item = mLrcList.get(i); if (item.getLrcTime() > seekto) { break; } } mCount = i; mPrePos = ⑴; mNextPos = 0; if (mCount > 0) { for (int j = 0; j < mCount; j++) { mNextPos = mLrcStr.indexOf("\n", mPrePos + 1); mPrePos = mLrcStr.indexOf("\n", mNextPos); } } startAnimation(-mLineHeight*i, 100); } @Override public void onMusicPause() { animTranY.pause(); } @Override public void onMusicResume() { animTranY.resume(); } private int mCount = 0; private float mCurrentHeight = 0; private float mLineHeight = 0; private Runnable mRefreshLrc = new Runnable() { @Override public void run() { if (mLineHeight == 0) { mLineHeight = (float) (tv_music.getHeight()-tv_music.getPaddingTop()) /mLrcList.size()/2; Log.d(TAG, "tv_music.getHeight()="+tv_music.getHeight()); Log.d(TAG, "tv_music.getPaddingTop()="+tv_music.getPaddingTop()); Log.d(TAG, "mLineHeight="+mLineHeight); } int offset = mLrcList.get(mCount).getLrcTime() - ((mCount==0)?0:mLrcList.get(mCount⑴).getLrcTime()) - 50; if (offset <= 0) { return; } startAnimation(mCurrentHeight - mLineHeight, offset); Log.d(TAG, "mLineHeight="+mLineHeight+",mCurrentHeight="+mCurrentHeight+",getHeight="+tv_music.getHeight()); } }; private int mPrePos = ⑴, mNextPos = 0; private String mLrcStr; private ObjectAnimator animTranY; public void startAnimation(float aimHeight, int offset) { Log.d(TAG, "mCurrentHeight="+mCurrentHeight+", aimHeight="+aimHeight); animTranY = ObjectAnimator.ofFloat(tv_music, "translationY", mCurrentHeight, aimHeight); animTranY.setDuration(offset); animTranY.setRepeatCount(0); animTranY.addListener(this); animTranY.start(); mCurrentHeight = aimHeight; } @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { if (mCount < mLrcList.size()) { mNextPos = mLrcStr.indexOf("\n", mPrePos+1); SpannableString spanText = new SpannableString(mLrcStr); spanText.setSpan(new ForegroundColorSpan(Color.RED), mPrePos+1, mNextPos>0?mNextPos:mLrcStr.length()⑴, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); mCount++; tv_music.setText(spanText); if (mNextPos > 0 && mNextPos < mLrcStr.length()⑴) { 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<String, Integer, Void> { @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播放的是流,所以,我們需要1邊播放1邊讀取 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(); } }



點擊下載本文用到的自定義音樂播放器的工程代碼



點此查看Android開發筆記的完全目錄
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈
程序員人生
------分隔線----------------------------
分享到:
------分隔線----------------------------
關閉
程序員人生
主站蜘蛛池模板: 性8成人有声小说在线播放 性freemovies中国群众 | 999毛片免费观看 | 自拍 另类 综合 欧美小说 | 波多野结衣视频一区二区 | 欧美在线成人免费国产 | 国产永久视频 | 精品伊人久久久香线蕉 | 欧美日韩一区二区三区视视频 | 德国女人一级毛片免费 | 亚洲成a人不卡在线观看 | 免费福利影院 | 性欧美巨大 | 日韩精品一区二区三区四区 | 欧美日韩国产高清一区二区三区 | 国产 | 久而欧洲野花视频欧洲1 | 中文字幕巨大乳在线看 | 一级做a爱片 | 久久www免费人成_看片高清 | 日本网络视频www色高清免费 | 99热久久最新地址获6取 | 国产大学生露脸激情 | 欧美日韩在线永久免费播放 | 亚洲国产精品尤物yw在线观看 | 爱爱免费网站 | 久久久久国产精品美女毛片 | 欧美日韩国产精品 | 国产欧美亚洲精品a | 国产成人免费不卡在线观看 | 亚欧成人乱码一区二区 | 暖暖在线精品日本中文 | japanesexxxxhd乱 | 日韩v片| 久草国产精品 | 激情啪啪精品一区二区 | 日本jizz在线播放 | 一级女性全黄生活片免费看 | 全亚洲最大的免费影院 | 在线免费亚洲 | 99r8这里精品热视频免费看 | 风间由美一区二区av101 | 99国产国人青青视频在线观看 |