程序员人生 网站导航

Android开发笔记(一百二十六)自定义音乐播放器

栏目:综合技术时间:2017-01-12 11:57:48

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开发笔记的完全目录
------分隔线----------------------------
------分隔线----------------------------

最新技术推荐