音频播放AudioTrack之入门篇

2020-12-27 20:36:10

参考地址 音频播放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

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

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

    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)的音频流。

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

总结

  1. 播放一个PCM文件,按照上面的五步走。

  2. 注意参数有配置,如量化位数是8BIT还是16BIT等。

  3. 想更加了解AudioTrack里的方法就动手写一个demo深入了解那些方法的用途。

  4. 能不能续播(还没有验证)

  • 2019-05-09 11:46:30

    Glide使用高级技巧(解决Glide生成缓存Key问题)

    虽说Glide将缓存功能高度封装之后,使得用法变得非常简单,但同时也带来了一些问题。 比如之前有一位群里的朋友就跟我说过,他们项目的图片资源都是存放在七牛云上面的,而七牛云为了对图片资源进行保护,会在图片url地址的基础之上再加上一个token参数。也就是说,一张图片的url地址可能会是如下格式:

  • 2019-05-13 14:34:42

    linux系统中清理MySql的日志文件,打印日志文件

    默认情况下mysql会一直保留mysql-bin文件,这样到一定时候,磁盘可能会被撑满,这时候是否可以删除这些文件呢,是否可以安全删除,是个问题。 首先要说明一下,这些文件都是mysql的日志文件,如果不做主从复制的话,基本上是没用的,虽然没用,但是不建议使用rm命令删除,这样有可能会不安全,正确的方法是通过mysql的命令去删除。

  • 2019-05-14 16:47:27

    数据库整理碎片

    最后还是用的ALTER TABLE来修改的,不知道为什么有时候管用,有时候不管用。

  • 2019-05-17 16:27:26

    在vue项目里面使用引入公共方法

    今天早上来到公司,没事看了一下别人的博客,然后试了一下,发现的确是可以的,在此记录一下,方便自己日后查阅。 首先新建一个文件夹:commonFunction ,然后在里面建立 一个文件common.js

  • 2019-05-18 12:37:39

    Android夜间模式的实现方案

    对于一款阅读类的软件,夜间模式是不可缺少的。最初看到这个需求时候觉得无从下手,没有一点头绪。后来通过查阅资料发现Android官方在Support Library 23.2.0中已经加入了夜间主题。也就是只需要通过更换主题便可实现日间模式和夜间模式的切换。下面截取项目实现的夜间模式效果图:

  • 2019-05-18 12:38:41

    android 快速实现夜间模式

    最近项目中遇到了一个问题,夜间模式在8.0以上的手机中不起作用,查看了一下原因,是夜间模式实现方法的问题。分两种情况介绍一下

  • 2019-05-18 12:40:35

    Android夜间模式的几种实现

    通过增加一层遮光罩来实现。效果不是很理想,但是好用,毕竟很多手机都有自己的夜间模式了

  • 2019-05-19 02:25:15

    php使用TCPDF生成PDF文件教程

    orientation属性用来设置文档打印格式是“Portrait”还是“Landscape”。 Landscape为横式打印,Portrait为纵向打印