音频播放AudioTrack之入门篇

音频播放

音频播放声音分为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

    1. AudioManager.STREAM_MUSIC:用于音乐播放的音频流。
    2. AudioManager.STREAM_SYSTEM:用于系统声音的音频流。
    3. AudioManager.STREAM_RING:用于电话铃声的音频流。
    4. AudioManager.STREAM_VOICE_CALL:用于电话通话的音频流。
    5. AudioManager.STREAM_ALARM:用于警报的音频流。
    6. AudioManager.STREAM_NOTIFICATION:用于通知的音频流。
    7. AudioManager.STREAM_BLUETOOTH_SCO:用于连接到蓝牙电话时的手机音频流。
    8. AudioManager.STREAM_SYSTEM_ENFORCED:在某些国家实施的系统声音的音频流。
    9. AudioManager.STREAM_DTMF:DTMF音调的音频流。
    10. AudioManager.STREAM_TTS:文本到语音转换(TTS)的音频流。

    为什么分那么多种类型,其实原因很简单,比如你在听music的时候接到电话,这个时候music播放肯定会停止,此时你只能听到电话,如果你调节音量的话,这个调节肯定只对电话起作用。当电话打完了,再回到music,你肯定不用再调节音量了。

    其实系统将这几种声音的数据分开管理,STREAM参数对AudioTrack来说,它的含义就是告诉系统,我现在想使用的是哪种类型的声音,这样系统就可以对应管理他们了。

  • 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.cpp

static 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下可用。将音频数据刷进等待播放的队列,任何写入的数据如果没有提交的话,都会被舍弃,但是并不能保证所有用于数据的缓冲空间都可用于后续的写入。

总结

  1. 播放一个PCM文件,按照上面的五步走。
  2. 注意参数有配置,如量化位数是8BIT还是16BIT等。
  3. 想更加了解AudioTrack里的方法就动手写一个demo深入了解那些方法的用途。
  4. 能不能续播(还没有验证)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容