音頻播放AudioTrack之入門篇

音頻播放

音頻播放聲音分為MediaPlayer和AudioTrack兩種方案的。MediaPlayer可以播放多種格式的聲音文件,例如MP3,WAV,OGG,AAC,MIDI等。然而AudioTrack只能播放PCM數(shù)據(jù)流。當(dāng)然兩者之間還是有緊密的聯(lián)系,MediaPlayer在播放音頻時(shí),在framework層還是會(huì)創(chuàng)建AudioTrack,把解碼后的PCM數(shù)流傳遞給AudioTrack,最后由AudioFlinger進(jìn)行混音,傳遞音頻給硬件播放出來(lái)。利用AudioTrack播放只是跳過(guò)Mediaplayer的解碼部分而已。

AudioTrack作用

AudioTrack是管理和播放單一音頻資源的類。AudioTrack僅僅能播放已經(jīng)解碼的PCM流,用于PCM音頻流的回放。

AudioTrack實(shí)現(xiàn)PCM音頻播放

AudioTrack實(shí)現(xiàn)PCM音頻播放五步走

  • 配置基本參數(shù)
  • 獲取最小緩沖區(qū)大小
  • 創(chuàng)建AudioTrack對(duì)象
  • 獲取PCM文件,轉(zhuǎn)成DataInputStream
  • 開(kāi)啟/停止播放

直接上代碼再分析

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;//播放文件的數(shù)據(jù)流
    private Thread mRecordThread;
    private boolean isStart = false;
    private volatile static AudioTrackManager mInstance;

    //音頻流類型
    private static final int mStreamType = AudioManager.STREAM_MUSIC;
    //指定采樣率 (MediaRecoder 的采樣率通常是8000Hz AAC的通常是44100Hz。 設(shè)置采樣率為44100,目前為常用的采樣率,官方文檔表示這個(gè)值可以兼容所有的設(shè)置)
    private static final int mSampleRateInHz=44100 ;
    //指定捕獲音頻的聲道數(shù)目。在AudioFormat類中指定用于此的常量
    private static final int mChannelConfig= AudioFormat.CHANNEL_CONFIGURATION_MONO; //單聲道
    //指定音頻量化位數(shù) ,在AudioFormaat類中指定了以下各種可能的常量。通常我們選擇ENCODING_PCM_16BIT和ENCODING_PCM_8BIT PCM代表的是脈沖編碼調(diào)制,它實(shí)際上是原始音頻樣本。
    //因此可以設(shè)置每個(gè)樣本的分辨率為16位或者8位,16位將占用更多的空間和處理能力,表示的音頻也更加接近真實(shí)。
    private static final int mAudioFormat=AudioFormat.ENCODING_PCM_16BIT;
    //指定緩沖區(qū)大小。調(diào)用AudioRecord類的getMinBufferSize方法可以獲得。
    private int mMinBufferSize;
    //STREAM的意思是由用戶在應(yīng)用程序通過(guò)write方式把數(shù)據(jù)一次一次得寫到audiotrack中。這個(gè)和我們?cè)趕ocket中發(fā)送數(shù)據(jù)一樣,
    // 應(yīng)用層從某個(gè)地方獲取數(shù)據(jù),例如通過(guò)編解碼得到PCM數(shù)據(jù),然后write到audiotrack。
    private static int mMode = AudioTrack.MODE_STREAM;


    public AudioTrackManager() {
        initData();
    }

    private void initData(){
        //根據(jù)采樣率,采樣精度,單雙聲道來(lái)得到frame的大小。
        mMinBufferSize = AudioTrack.getMinBufferSize(mSampleRateInHz,mChannelConfig, mAudioFormat);//計(jì)算最小緩沖區(qū)
        //注意,按照數(shù)字音頻的知識(shí),這個(gè)算出來(lái)的是一秒鐘buffer的大小。
        //創(chuàng)建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;
        }
    }

    /**
     * 啟動(dòng)播放線程
     */
    private void startThread() {
        destroyThread();
        isStart = true;
        if (mRecordThread == null) {
            mRecordThread = new Thread(recordRunnable);
            mRecordThread.start();
        }
    }

    /**
     * 播放線程
     */
    Runnable recordRunnable = new Runnable() {
        @Override
        public void run() {
            try {
                //設(shè)置線程的優(yōu)先級(jí)
                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) {//一邊播放一邊寫入語(yǔ)音數(shù)據(jù)
                        //判斷AudioTrack未初始化,停止播放的時(shí)候釋放了,狀態(tài)就為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));
    }

    /**
     * 啟動(dòng)播放
     *
     * @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的參數(shù)是否支持當(dāng)前的硬件設(shè)備
//            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();//關(guān)閉數(shù)據(jù)輸入流
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

配置基本參數(shù)

  • StreamType音頻流類型

    最主要的幾種STREAM

    1. AudioManager.STREAM_MUSIC:用于音樂(lè)播放的音頻流。
    2. AudioManager.STREAM_SYSTEM:用于系統(tǒng)聲音的音頻流。
    3. AudioManager.STREAM_RING:用于電話鈴聲的音頻流。
    4. AudioManager.STREAM_VOICE_CALL:用于電話通話的音頻流。
    5. AudioManager.STREAM_ALARM:用于警報(bào)的音頻流。
    6. AudioManager.STREAM_NOTIFICATION:用于通知的音頻流。
    7. AudioManager.STREAM_BLUETOOTH_SCO:用于連接到藍(lán)牙電話時(shí)的手機(jī)音頻流。
    8. AudioManager.STREAM_SYSTEM_ENFORCED:在某些國(guó)家實(shí)施的系統(tǒng)聲音的音頻流。
    9. AudioManager.STREAM_DTMF:DTMF音調(diào)的音頻流。
    10. AudioManager.STREAM_TTS:文本到語(yǔ)音轉(zhuǎn)換(TTS)的音頻流。

    為什么分那么多種類型,其實(shí)原因很簡(jiǎn)單,比如你在聽(tīng)music的時(shí)候接到電話,這個(gè)時(shí)候music播放肯定會(huì)停止,此時(shí)你只能聽(tīng)到電話,如果你調(diào)節(jié)音量的話,這個(gè)調(diào)節(jié)肯定只對(duì)電話起作用。當(dāng)電話打完了,再回到music,你肯定不用再調(diào)節(jié)音量了。

    其實(shí)系統(tǒng)將這幾種聲音的數(shù)據(jù)分開(kāi)管理,STREAM參數(shù)對(duì)AudioTrack來(lái)說(shuō),它的含義就是告訴系統(tǒng),我現(xiàn)在想使用的是哪種類型的聲音,這樣系統(tǒng)就可以對(duì)應(yīng)管理他們了。

  • MODE模式(static和stream兩種)

    • AudioTrack.MODE_STREAM

      STREAM的意思是由用戶在應(yīng)用程序通過(guò)write方式把數(shù)據(jù)一次一次得寫到AudioTrack中。這個(gè)和我們?cè)趕ocket中發(fā)送數(shù)據(jù)一樣,應(yīng)用層從某個(gè)地方獲取數(shù)據(jù),例如通過(guò)編解碼得到PCM數(shù)據(jù),然后write到AudioTrack。這種方式的壞處就是總是在JAVA層和Native層交互,效率損失較大。

    • AudioTrack.MODE_STATIC

      STATIC就是數(shù)據(jù)一次性交付給接收方。好處是簡(jiǎn)單高效,只需要進(jìn)行一次操作就完成了數(shù)據(jù)的傳遞;缺點(diǎn)當(dāng)然也很明顯,對(duì)于數(shù)據(jù)量較大的音頻回放,顯然它是無(wú)法勝任的,因而通常只用于播放鈴聲、系統(tǒng)提醒等對(duì)內(nèi)存小的操作

  • 采樣率:mSampleRateInHz

    采樣率 (MediaRecoder 的采樣率通常是8000Hz AAC的通常是44100Hz。 設(shè)置采樣率為44100,目前為常用的采樣率,官方文檔表示這個(gè)值可以兼容所有的設(shè)置)

  • 通道數(shù)目:mChannelConfig

    首先得出聲道數(shù),目前最多只支持雙聲道。為什么最多只支持雙聲道?看下面的源碼

      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);
              }
          }
    
      .......
    
      }
    
  • 音頻量化位數(shù):mAudioFormat(只支持8bit和16bit兩種。)

      if ((audioFormat !=AudioFormat.ENCODING_PCM_16BIT)
    
      && (audioFormat !=AudioFormat.ENCODING_PCM_8BIT)) {
    
      returnAudioTrack.ERROR_BAD_VALUE;
    
      }
    

最小緩沖區(qū)大小

mMinBufferSize取決于采樣率、聲道數(shù)和采樣深度三個(gè)屬性,那么具體是如何計(jì)算的呢?我們看一下源碼

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;
    }
}

看到源碼緩沖區(qū)的大小的實(shí)現(xiàn)在nativen層中,接著看下native層代碼實(shí)現(xiàn):

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);

}

這里又調(diào)用了getMinFrameCount,這個(gè)函數(shù)用于確定至少需要多少Frame才能保證音頻正常播放。那么Frame代表了什么意思呢?可以想象一下視頻中幀的概念,它代表了某個(gè)時(shí)間點(diǎn)的一幅圖像。這里的Frame也是類似的,它應(yīng)該是指某個(gè)特定時(shí)間點(diǎn)時(shí)的音頻數(shù)據(jù)量,所以android_media_AudioTrack_get_min_buff_size中最后采用的計(jì)算公式就是:

至少需要多少幀每幀數(shù)據(jù)量 = frameCount * nbChannels * (audioFormat ==javaAudioTrackFields.PCM16 ? 2 : 1);
公式中frameCount就是需要的幀數(shù),每一幀的數(shù)據(jù)量又等于:
Channel數(shù)
每個(gè)Channel數(shù)據(jù)量= nbChannels * (audioFormat ==javaAudioTrackFields.PCM16 ? 2 : 1)層層返回getMinBufferSize就得到了保障AudioTrack正常工作的最小緩沖區(qū)大小了。

創(chuàng)建AudioTrack對(duì)象

取到mMinBufferSize后,我們就可以創(chuàng)建一個(gè)AudioTrack對(duì)象了。它的構(gòu)造函數(shù)原型是:

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,看看實(shí)現(xiàn)些什么了

/*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();

這里調(diào)用了native_setup來(lái)創(chuàng)建一個(gè)本地AudioTrack對(duì)象,創(chuàng)建一個(gè)Storage對(duì)象,從這個(gè)Storage猜測(cè)這可能是存儲(chǔ)音頻數(shù)據(jù)的地方,我們?cè)龠M(jìn)入了解這個(gè)Storage對(duì)象。

if (memoryMode== javaAudioTrackFields.MODE_STREAM) {

    lpTrack->set(
    ...

    audioCallback, //回調(diào)函數(shù)

    &(lpJniStorage->mCallbackData),//回調(diào)數(shù)據(jù)

        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結(jié)束

調(diào)用set函數(shù)為AudioTrack設(shè)置這些屬性——我們只保留兩種內(nèi)存模式(STATIC和STREAM)有差異的地方,入?yún)⒅械牡箶?shù)第三個(gè)是lpJniStorage->mMemBase,而STREAM類型時(shí)為null(0)。太深了,對(duì)于基礎(chǔ)的知識(shí)先研究到這里吧

獲取PCM文件,轉(zhuǎn)成DataInputStream

根據(jù)存放PCM的路徑獲取到PCM文件

/**
 * 播放文件
 * @param path
 * @throws Exception
 */
private void setPath(String path) throws Exception {
    File file = new File(path);
    mDis = new DataInputStream(new FileInputStream(file));
}

開(kāi)啟/停止播放

  • 開(kāi)始播放

      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();
          }
      }
    
  • 停止播放

    停止播放音頻數(shù)據(jù),如果是STREAM模式,會(huì)等播放完最后寫入buffer的數(shù)據(jù)才會(huì)停止。如果立即停止,要調(diào)用pause()方法,然后調(diào)用flush方法,會(huì)舍棄還沒(méi)有播放的數(shù)據(jù)。

    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;
          }
    }
    
  • 暫停播放

    暫停播放,調(diào)用play()重新開(kāi)始播放。

  • 釋放本地AudioTrack資源

    AudioTrack.release()

  • 返回當(dāng)前的播放狀態(tài)

    AudioTrack.getPlayState()

注意: flush()只在模式為STREAM下可用。將音頻數(shù)據(jù)刷進(jìn)等待播放的隊(duì)列,任何寫入的數(shù)據(jù)如果沒(méi)有提交的話,都會(huì)被舍棄,但是并不能保證所有用于數(shù)據(jù)的緩沖空間都可用于后續(xù)的寫入。

總結(jié)

  1. 播放一個(gè)PCM文件,按照上面的五步走。
  2. 注意參數(shù)有配置,如量化位數(shù)是8BIT還是16BIT等。
  3. 想更加了解AudioTrack里的方法就動(dòng)手寫一個(gè)demo深入了解那些方法的用途。
  4. 能不能續(xù)播(還沒(méi)有驗(yàn)證)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android音頻收集和播放(一) 一、文章說(shuō)明 這是自己第一次通過(guò)寫文章的方式來(lái)記錄在開(kāi)發(fā)中的一些心得,在這里也...
    熊熊熊孩子閱讀 12,701評(píng)論 13 49
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,534評(píng)論 19 139
  • 安卓平臺(tái)和聲音錄制與播放相關(guān)的主要是4個(gè)類:MediaRecorder,MediaPlayer,SoundPool...
    閑庭閱讀 10,880評(píng)論 1 47
  • 前言 本篇開(kāi)始講解在Android平臺(tái)上進(jìn)行的音頻編輯開(kāi)發(fā),首先需要對(duì)音頻相關(guān)概念有基礎(chǔ)的認(rèn)識(shí)。所以本篇要講解以下...
    Ihesong閱讀 8,047評(píng)論 2 18
  • 本文屬于Android局域網(wǎng)內(nèi)的語(yǔ)音對(duì)講項(xiàng)目系列,《通過(guò)UDP廣播實(shí)現(xiàn)Android局域網(wǎng)Peer Discove...
    yhthu閱讀 19,652評(píng)論 9 53

友情鏈接更多精彩內(nèi)容