音频播放
音频播放声音分为MediaPlayer和AudioTrack两种方案的。MediaPlayer可以播放多种格式的声音文件,例如MP3,WAV,OGG,AAC,MIDI等。然而AudioTrack只能播放PCM数据流。当然两者之间还是有紧密的联系,MediaPlayer在播放音频时,在framework层还是会创建AudioTrack,把解码后的PCM数流传递给AudioTrack,最后由AudioFlinger进行混音,传递音频给硬件播放出来。利用AudioTrack播放只是跳过Mediaplayer的解码部分而已。
AudioTrack作用
AudioTrack是管理和播放单一音频资源的类。AudioTrack仅仅能播放已经解码的PCM流,用于PCM音频流的回放。
AudioTrack实现PCM音频播放
AudioTrack实现PCM音频播放五步走
配置基本参数
获取最小缓冲区大小
创建AudioTrack对象
获取PCM文件,转成DataInputStream
开启/停止播放
直接上代码再分析
import android.media.AudioFormat;import android.media.AudioManager;import android.media.AudioRecord;import android.media.AudioTrack;import java.io.DataInputStream;import java.io.File;import java.io.FileInputStream;public class AudioTrackManager { private AudioTrack mAudioTrack; private DataInputStream mDis;//播放文件的数据流 private Thread mRecordThread; private boolean isStart = false; private volatile static AudioTrackManager mInstance; //音频流类型 private static final int mStreamType = AudioManager.STREAM_MUSIC; //指定采样率 (MediaRecoder 的采样率通常是8000Hz AAC的通常是44100Hz。 设置采样率为44100,目前为常用的采样率,官方文档表示这个值可以兼容所有的设置) private static final int mSampleRateInHz=44100 ; //指定捕获音频的声道数目。在AudioFormat类中指定用于此的常量 private static final int mChannelConfig= AudioFormat.CHANNEL_CONFIGURATION_MONO; //单声道 //指定音频量化位数 ,在AudioFormaat类中指定了以下各种可能的常量。通常我们选择ENCODING_PCM_16BIT和ENCODING_PCM_8BIT PCM代表的是脉冲编码调制,它实际上是原始音频样本。 //因此可以设置每个样本的分辨率为16位或者8位,16位将占用更多的空间和处理能力,表示的音频也更加接近真实。 private static final int mAudioFormat=AudioFormat.ENCODING_PCM_16BIT; //指定缓冲区大小。调用AudioRecord类的getMinBufferSize方法可以获得。 private int mMinBufferSize; //STREAM的意思是由用户在应用程序通过write方式把数据一次一次得写到audiotrack中。这个和我们在socket中发送数据一样, // 应用层从某个地方获取数据,例如通过编解码得到PCM数据,然后write到audiotrack。 private static int mMode = AudioTrack.MODE_STREAM; public AudioTrackManager() { initData(); } private void initData(){ //根据采样率,采样精度,单双声道来得到frame的大小。 mMinBufferSize = AudioTrack.getMinBufferSize(mSampleRateInHz,mChannelConfig, mAudioFormat);//计算最小缓冲区 //注意,按照数字音频的知识,这个算出来的是一秒钟buffer的大小。 //创建AudioTrack mAudioTrack = new AudioTrack(mStreamType, mSampleRateInHz,mChannelConfig, mAudioFormat,mMinBufferSize,mMode); } /** * 获取单例引用 * * @return */ public static AudioTrackManager getInstance() { if (mInstance == null) { synchronized (AudioTrackManager.class) { if (mInstance == null) { mInstance = new AudioTrackManager(); } } } return mInstance; } /** * 销毁线程方法 */ private void destroyThread() { try { isStart = false; if (null != mRecordThread && Thread.State.RUNNABLE == mRecordThread.getState()) { try { Thread.sleep(500); mRecordThread.interrupt(); } catch (Exception e) { mRecordThread = null; } } mRecordThread = null; } catch (Exception e) { e.printStackTrace(); } finally { mRecordThread = null; } } /** * 启动播放线程 */ private void startThread() { destroyThread(); isStart = true; if (mRecordThread == null) { mRecordThread = new Thread(recordRunnable); mRecordThread.start(); } } /** * 播放线程 */ Runnable recordRunnable = new Runnable() { @Override public void run() { try { //设置线程的优先级 android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO); byte[] tempBuffer = new byte[mMinBufferSize]; int readCount = 0; while (mDis.available() > 0) { readCount= mDis.read(tempBuffer); if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) { continue; } if (readCount != 0 && readCount != -1) {//一边播放一边写入语音数据 //判断AudioTrack未初始化,停止播放的时候释放了,状态就为STATE_UNINITIALIZED if(mAudioTrack.getState() == mAudioTrack.STATE_UNINITIALIZED){ initData(); } mAudioTrack.play(); mAudioTrack.write(tempBuffer, 0, readCount); } } stopPlay();//播放完就停止播放 } catch (Exception e) { e.printStackTrace(); } } }; /** * 播放文件 * @param path * @throws Exception */ private void setPath(String path) throws Exception { File file = new File(path); mDis = new DataInputStream(new FileInputStream(file)); } /** * 启动播放 * * @param path */ public void startPlay(String path) { try {// //AudioTrack未初始化// if(mAudioTrack.getState() == AudioTrack.STATE_UNINITIALIZED){// throw new RuntimeException("The AudioTrack is not uninitialized");// }//AudioRecord.getMinBufferSize的参数是否支持当前的硬件设备// else if (AudioTrack.ERROR_BAD_VALUE == mMinBufferSize || AudioTrack.ERROR == mMinBufferSize) {// throw new RuntimeException("AudioTrack Unable to getMinBufferSize");// }else{ setPath(path); startThread();// } } catch (Exception e) { e.printStackTrace(); } } /** * 停止播放 */ public void stopPlay() { try { destroyThread();//销毁线程 if (mAudioTrack != null) { if (mAudioTrack.getState() == AudioRecord.STATE_INITIALIZED) {//初始化成功 mAudioTrack.stop();//停止播放 } if (mAudioTrack != null) { mAudioTrack.release();//释放audioTrack资源 } } if (mDis != null) { mDis.close();//关闭数据输入流 } } catch (Exception e) { e.printStackTrace(); } }}
配置基本参数
StreamType音频流类型
最主要的几种STREAM
为什么分那么多种类型,其实原因很简单,比如你在听music的时候接到电话,这个时候music播放肯定会停止,此时你只能听到电话,如果你调节音量的话,这个调节肯定只对电话起作用。当电话打完了,再回到music,你肯定不用再调节音量了。
其实系统将这几种声音的数据分开管理,STREAM参数对AudioTrack来说,它的含义就是告诉系统,我现在想使用的是哪种类型的声音,这样系统就可以对应管理他们了。
AudioManager.STREAM_MUSIC:用于音乐播放的音频流。
AudioManager.STREAM_SYSTEM:用于系统声音的音频流。
AudioManager.STREAM_RING:用于电话铃声的音频流。
AudioManager.STREAM_VOICE_CALL:用于电话通话的音频流。
AudioManager.STREAM_ALARM:用于警报的音频流。
AudioManager.STREAM_NOTIFICATION:用于通知的音频流。
AudioManager.STREAM_BLUETOOTH_SCO:用于连接到蓝牙电话时的手机音频流。
AudioManager.STREAM_SYSTEM_ENFORCED:在某些国家实施的系统声音的音频流。
AudioManager.STREAM_DTMF:DTMF音调的音频流。
AudioManager.STREAM_TTS:文本到语音转换(TTS)的音频流。
MODE模式(static和stream两种)
AudioTrack.MODE_STREAM
STREAM的意思是由用户在应用程序通过write方式把数据一次一次得写到AudioTrack中。这个和我们在socket中发送数据一样,应用层从某个地方获取数据,例如通过编解码得到PCM数据,然后write到AudioTrack。这种方式的坏处就是总是在JAVA层和Native层交互,效率损失较大。
AudioTrack.MODE_STATIC
STATIC就是数据一次性交付给接收方。好处是简单高效,只需要进行一次操作就完成了数据的传递;缺点当然也很明显,对于数据量较大的音频回放,显然它是无法胜任的,因而通常只用于播放铃声、系统提醒等对内存小的操作
采样率:mSampleRateInHz
采样率 (MediaRecoder 的采样率通常是8000Hz AAC的通常是44100Hz。 设置采样率为44100,目前为常用的采样率,官方文档表示这个值可以兼容所有的设置)
通道数目:mChannelConfig
首先得出声道数,目前最多只支持双声道。为什么最多只支持双声道?看下面的源码
static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat) { int channelCount = 0; switch(channelConfig) { case AudioFormat.CHANNEL_OUT_MONO: case AudioFormat.CHANNEL_CONFIGURATION_MONO: channelCount = 1; break; case AudioFormat.CHANNEL_OUT_STEREO: case AudioFormat.CHANNEL_CONFIGURATION_STEREO: channelCount = 2; break; default: if (!isMultichannelConfigSupported(channelConfig)) { loge("getMinBufferSize(): Invalid channel configuration."); return ERROR_BAD_VALUE; } else { channelCount = AudioFormat.channelCountFromOutChannelMask(channelConfig); } } ....... }
音频量化位数:mAudioFormat(只支持8bit和16bit两种。)
if ((audioFormat !=AudioFormat.ENCODING_PCM_16BIT) && (audioFormat !=AudioFormat.ENCODING_PCM_8BIT)) { returnAudioTrack.ERROR_BAD_VALUE; }
最小缓冲区大小
mMinBufferSize取决于采样率、声道数和采样深度三个属性,那么具体是如何计算的呢?我们看一下源码
static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat) { .... int size = native_get_min_buff_size(sampleRateInHz, channelCount, audioFormat); if (size <= 0) { loge("getMinBufferSize(): error querying hardware"); return ERROR; } else { return size; }}
看到源码缓冲区的大小的实现在nativen层中,接着看下native层代码实现:
rameworks/base/core/jni/android_media_AudioTrack.cppstatic jint android_media_AudioTrack_get_min_buff_size(JNIEnv*env, jobject thiz,jint sampleRateInHertz,jint nbChannels, jint audioFormat) {int frameCount = 0;if(AudioTrack::getMinFrameCount(&frameCount, AUDIO_STREAM_DEFAULT,sampleRateInHertz) != NO_ERROR) { return -1; } return frameCount * nbChannels * (audioFormat ==javaAudioTrackFields.PCM16 ? 2 : 1);}
这里又调用了getMinFrameCount,这个函数用于确定至少需要多少Frame才能保证音频正常播放。那么Frame代表了什么意思呢?可以想象一下视频中帧的概念,它代表了某个时间点的一幅图像。这里的Frame也是类似的,它应该是指某个特定时间点时的音频数据量,所以android_media_AudioTrack_get_min_buff_size中最后采用的计算公式就是:
至少需要多少帧每帧数据量 = frameCount * nbChannels * (audioFormat ==javaAudioTrackFields.PCM16 ? 2 : 1);
公式中frameCount就是需要的帧数,每一帧的数据量又等于:
Channel数每个Channel数据量= nbChannels * (audioFormat ==javaAudioTrackFields.PCM16 ? 2 : 1)层层返回getMinBufferSize就得到了保障AudioTrack正常工作的最小缓冲区大小了。
创建AudioTrack对象
取到mMinBufferSize后,我们就可以创建一个AudioTrack对象了。它的构造函数原型是:
public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)throws IllegalArgumentException { this(streamType, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes, mode, AudioManager.AUDIO_SESSION_ID_GENERATE);}
在源码中一层层往下看
public AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes, int mode, int sessionId) throws IllegalArgumentException { super(attributes, AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK); ..... // native initialization int initResult = native_setup(new WeakReference<AudioTrack>(this), mAttributes, sampleRate, mChannelMask, mChannelIndexMask, mAudioFormat, mNativeBufferSizeInBytes, mDataLoadMode, session, 0 /*nativeTrackInJavaObj*/); if (initResult != SUCCESS) { loge("Error code "+initResult+" when initializing AudioTrack."); return; // with mState == STATE_UNINITIALIZED } mSampleRate = sampleRate[0]; mSessionId = session[0]; if (mDataLoadMode == MODE_STATIC) { mState = STATE_NO_STATIC_DATA; } else { mState = STATE_INITIALIZED; } baseRegisterPlayer();}
最终看到了又在native_setup方法中,在native中initialization,看看实现些什么了
/*frameworks/base/core/jni/android_media_AudioTrack.cpp*/static int android_media_AudioTrack_native_setup(JNIEnv*env, jobject thiz, jobject weak_this, jint streamType, jintsampleRateInHertz, jint javaChannelMask, jint audioFormat, jintbuffSizeInBytes, jint memoryMode, jintArray jSession){ ..... sp<AudioTrack>lpTrack = new AudioTrack(); .....AudioTrackJniStorage* lpJniStorage =new AudioTrackJniStorage();
这里调用了native_setup来创建一个本地AudioTrack对象,创建一个Storage对象,从这个Storage猜测这可能是存储音频数据的地方,我们再进入了解这个Storage对象。
if (memoryMode== javaAudioTrackFields.MODE_STREAM) { lpTrack->set( ... audioCallback, //回调函数 &(lpJniStorage->mCallbackData),//回调数据 0, 0,//shared mem true,// thread cancall Java sessionId);//audio session ID } else if (memoryMode ==javaAudioTrackFields.MODE_STATIC) { ... lpTrack->set( ... audioCallback, &(lpJniStorage->mCallbackData),0, lpJniStorage->mMemBase,// shared mem true,// thread cancall Java sessionId);//audio session ID }....// native_setup结束
调用set函数为AudioTrack设置这些属性——我们只保留两种内存模式(STATIC和STREAM)有差异的地方,入参中的倒数第三个是lpJniStorage->mMemBase,而STREAM类型时为null(0)。太深了,对于基础的知识先研究到这里吧
获取PCM文件,转成DataInputStream
根据存放PCM的路径获取到PCM文件
/** * 播放文件 * @param path * @throws Exception */private void setPath(String path) throws Exception { File file = new File(path); mDis = new DataInputStream(new FileInputStream(file));}
开启/停止播放
开始播放
public void play()throws IllegalStateException { if (mState != STATE_INITIALIZED) { throw new IllegalStateException("play() called on uninitialized AudioTrack."); } //FIXME use lambda to pass startImpl to superclass final int delay = getStartDelayMs(); if (delay == 0) { startImpl(); } else { new Thread() { public void run() { try { Thread.sleep(delay); } catch (InterruptedException e) { e.printStackTrace(); } baseSetStartDelayMs(0); try { startImpl(); } catch (IllegalStateException e) { // fail silently for a state exception when it is happening after // a delayed start, as the player state could have changed between the // call to start() and the execution of startImpl() } } }.start(); } }
停止播放
停止播放音频数据,如果是STREAM模式,会等播放完最后写入buffer的数据才会停止。如果立即停止,要调用pause()方法,然后调用flush方法,会舍弃还没有播放的数据。
public void stop()throws IllegalStateException { if (mState != STATE_INITIALIZED) { throw new IllegalStateException("stop() called on uninitialized AudioTrack."); } // stop playing synchronized(mPlayStateLock) { native_stop(); baseStop(); mPlayState = PLAYSTATE_STOPPED; mAvSyncHeader = null; mAvSyncBytesRemaining = 0; }}
暂停播放
暂停播放,调用play()重新开始播放。
释放本地AudioTrack资源
AudioTrack.release()
返回当前的播放状态
AudioTrack.getPlayState()
注意: flush()只在模式为STREAM下可用。将音频数据刷进等待播放的队列,任何写入的数据如果没有提交的话,都会被舍弃,但是并不能保证所有用于数据的缓冲空间都可用于后续的写入。
总结
播放一个PCM文件,按照上面的五步走。
注意参数有配置,如量化位数是8BIT还是16BIT等。
想更加了解AudioTrack里的方法就动手写一个demo深入了解那些方法的用途。
能不能续播(还没有验证)