音視訊入門之音訊採集、編碼、播放

玉剛說發表於2018-12-28

今天我們學習音訊的採集、編碼、生成檔案、轉碼等操作,我們生成三種格式的檔案格式,pcm、wav、aac 三種格式,並且我們用 AudioStack 來播放音訊,最後我們播放這個音訊。

本篇文章你將學到:

  • AudioRecord 音訊的採集
  • 生成 PCM 檔案
  • PCM 轉化為 WAV 檔案
  • PCM 轉化為 AAC 檔案
  • 附上正常執行 demo 原始碼

使用 AudioRecord 實現錄音生成PCM 檔案

AudioRecord 是 Android 系統提供的用於實現錄音的功能類,要想了解這個類的具體的說明和用法,我們可以去看一下官方的文件:

AndioRecord類的主要功能是讓各種 Java 應用能夠管理音訊資源,以便它們通過此類能夠錄製聲音相關的硬體所收集的聲音。此功能的實現就是通過”pulling”(讀取)AudioRecord物件的聲音資料來完成的。在錄音過程中,應用所需要做的就是通過後面三個類方法中的一個去及時地獲取AudioRecord物件的錄音資料. AudioRecord類提供的三個獲取聲音資料的方法分別是read(byte[], int, int), read(short[], int, int), read(ByteBuffer, int). 無論選擇使用那一個方法都必須事先設定方便使用者的聲音資料的儲存格式。

開始錄音的時候,AudioRecord需要初始化一個相關聯的聲音buffer, 這個buffer主要是用來儲存新的聲音資料。這個buffer的大小,我們可以在物件構造期間去指定。它表明一個AudioRecord物件還沒有被讀取(同步)聲音資料前能錄多長的音(即一次可以錄製的聲音容量)。聲音資料從音訊硬體中被讀出,資料大小不超過整個錄音資料的大小(可以分多次讀出),即每次讀取初始化buffer容量的資料。

1.1 首先要宣告一些全域性的變數和常量引數

主要是宣告一些用到的引數,具體解釋可以看註釋。

//指定音訊源 這個和MediaRecorder是相同的 MediaRecorder.AudioSource.MIC指的是麥克風
private static final int mAudioSource = MediaRecorder.AudioSource.MIC;
//指定取樣率 (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 mBufferSizeInBytes;

// 宣告 AudioRecord 物件
private AudioRecord mAudioRecord = null;
複製程式碼

1.2 獲取buffer的大小並建立AudioRecord

//初始化資料,計算最小緩衝區
mBufferSizeInBytes = AudioRecord.getMinBufferSize(mSampleRateInHz, mChannelConfig, mAudioFormat);
//建立AudioRecorder物件
mAudioRecord = new AudioRecord(mAudioSource, mSampleRateInHz, mChannelConfig,
                               mAudioFormat, mBufferSizeInBytes);
複製程式碼

1.3 建立一個子執行緒開啟執行緒錄音,並寫入檔案檔案

 @Override
    public void run() {
        //標記為開始採集狀態
        isRecording = true;
        //建立檔案
        createFile();

        try {

            //判斷AudioRecord未初始化,停止錄音的時候釋放了,狀態就為STATE_UNINITIALIZED
            if (mAudioRecord.getState() == mAudioRecord.STATE_UNINITIALIZED) {
                initData();
            }

            //最小緩衝區
            byte[] buffer = new byte[mBufferSizeInBytes];
            //獲取到檔案的資料流
            mDataOutputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(mRecordingFile)));

            //開始錄音
            mAudioRecord.startRecording();
            //getRecordingState獲取當前AudioReroding是否正在採集資料的狀態
            while (isRecording && mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                int bufferReadResult = mAudioRecord.read(buffer, 0, mBufferSizeInBytes);
                for (int i = 0; i < bufferReadResult; i++) {
                    mDataOutputStream.write(buffer[i]);
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "Recording Failed");
        } finally {
            // 停止錄音
            stopRecord();
            IOUtil.close(mDataOutputStream);
        }
    }
複製程式碼

1.4 許可權和採集小結

注意:許可權需求:WRITE_EXTERNAL_STORAGE、RECORD_AUDIO

到現在基本的錄音的流程就介紹完了,但是這時候問題來了:

1) 我按照流程,把音訊資料都輸出到檔案裡面了,停止錄音後,開啟此檔案,發現不能播放,到底是為什麼呢?

答:按照流程走完了,資料是進去了,但是現在的檔案裡面的內容僅僅是最原始的音訊資料,術語稱為raw(中文解釋是“原材料”或“未經處理的東西”),這時候,你讓播放器去開啟,它既不知道儲存的格式是什麼,又不知道如何進行解碼操作。當然播放不了。

2) 那如何才能在播放器中播放我錄製的內容呢?

答: 在檔案的資料開頭加入AAC HEAD 或者 AAC 資料即可,也就是檔案頭。只有加上檔案頭部的資料,播放器才能正確的知道里面的內容到底是什麼,進而能夠正常的解析並播放裡面的內容。

PCM 、WAV、AAC 的檔案頭介紹

我這裡簡單的介紹一下這三種的格式的基本介紹,具體我新增了具體的訪問連結,具體點選詳情檢視,我這裡點到為止。

PCM:PCM(Pulse Code Modulation----脈碼調製錄音)。所謂PCM錄音就是將聲音等模擬訊號變成符號化的脈衝列,再予以記錄。PCM訊號是由[1]、[0]等符號構成的數字訊號,而未經過任何編碼和壓縮處理。與模擬訊號比,它不易受傳送系統的雜波及失真的影響。動態範圍寬,可得到音質相當好的影響效果。

WAV : wav是一種無損的音訊檔案格式,WAV符合 PIFF(Resource Interchange File Format)規範。所有的WAV都有一個檔案頭,這個檔案頭音訊流的編碼引數。WAV對音訊流的編碼沒有硬性規定,除了PCM之外,還有幾乎所有支援ACM規範的編碼都可以為WAV的音訊流進行編碼。

簡單來說:WAV 是一種無損的音訊檔案格式,PCM是沒有壓縮的編碼方式

AAC : AAC(Advanced Audio Coding),中文稱為“高階音訊編碼”,出現於1997年,基於 MPEG-2的音訊編碼技術。由Fraunhofer IIS、杜比實驗室、AT&T、Sony(索尼)等公司共同開發,目的是取代MP3格式。2000年,MPEG-4標準出現後,AAC 重新整合了其特性,加入了SBR技術和PS技術,為了區別於傳統的 MPEG-2 AAC 又稱為 MPEG-4 AAC。他是一種專為聲音資料設計的檔案壓縮格式,與Mp3類似。利用AAC格式,可使聲音檔案明顯減小,而不會讓人感覺聲音質量有所降低 。

PCM 轉化為 WAV

在檔案的資料開頭加入WAVE HEAD 或者 AAC 資料即可,也就是檔案頭。只有加上檔案頭部的資料,播放器才能正確的知道里面的內容到底是什麼,進而能夠正常的解析並播放裡面的內容。具體的標頭檔案的描述,在Play a WAV file on an AudioTrack裡面可以進行了解。

public class WAVUtil {

    /**
     * PCM檔案轉WAV檔案
     *
     * @param inPcmFilePath  輸入PCM檔案路徑
     * @param outWavFilePath 輸出WAV檔案路徑
     * @param sampleRate     取樣率,例如44100
     * @param channels       聲道數 單聲道:1或雙聲道:2
     * @param bitNum         取樣位數,8或16
     */

    public static void convertPcm2Wav(String inPcmFilePath, String outWavFilePath, int sampleRate,int channels, int bitNum{

        FileInputStream in = null;
        FileOutputStream out = null;
        byte[] data = new byte[1024];

        try {
            //取樣位元組byte率
            long byteRate = sampleRate * channels * bitNum / 8;

            in = new FileInputStream(inPcmFilePath);
            out = new FileOutputStream(outWavFilePath);

            //PCM檔案大小
            long totalAudioLen = in.getChannel().size();

            //總大小,由於不包括RIFF和WAV,所以是44 - 8 = 36,在加上PCM檔案大小
            long totalDataLen = totalAudioLen + 36;

            writeWaveFileHeader(out, totalAudioLen, totalDataLen, sampleRate, channels, byteRate);

            int length = 0;
            while ((length = in.read(data)) > 0) {
                out.write(data, 0, length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            IOUtil.close(in,out);
        }
    }


    /**
     * 輸出WAV檔案
     *
     * @param out           WAV輸出檔案流
     * @param totalAudioLen 整個音訊PCM資料大小
     * @param totalDataLen  整個資料大小
     * @param sampleRate    取樣率
     * @param channels      聲道數
     * @param byteRate      取樣位元組byte率
     * @throws IOException
     */

    private static void writeWaveFileHeader(FileOutputStream outlong totalAudioLen,long totalDataLen, int sampleRate, int channels, long byteRate) throws IOException {
        byte[] header = new byte[44];
        header[0] = 'R'// RIFF
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalDataLen & 0xff);//資料大小
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        header[8] = 'W';//WAVE
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        //FMT Chunk
        header[12] = 'f'// 'fmt '
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';//過渡位元組
        //資料大小
        header[16] = 16// 4 bytes: size of 'fmt ' chunk
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        //編碼方式 10H為PCM編碼格式
        header[20] = 1// format = 1
        header[21] = 0;
        //通道數
        header[22] = (byte) channels;
        header[23] = 0;
        //取樣率,每個通道的播放速度
        header[24] = (byte) (sampleRate & 0xff);
        header[25] = (byte) ((sampleRate >> 8) & 0xff);
        header[26] = (byte) ((sampleRate >> 16) & 0xff);
        header[27] = (byte) ((sampleRate >> 24) & 0xff);
        //音訊資料傳送速率,取樣率*通道數*取樣深度/8
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        // 確定系統一次要處理多少個這樣位元組的資料,確定緩衝區,通道數*取樣位數
        header[32] = (byte) (channels * 16 / 8);
        header[33] = 0;
        //每個樣本的資料位數
        header[34] = 16;
        header[35] = 0;
        //Data chunk
        header[36] = 'd';//data
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
        out.write(header, 044);
    }
}
複製程式碼

看到下圖我們生成了相對的 wav 檔案,我們用用本機自帶播放器開啟此時就能正常播放,但是我們發現他的大小比較大,我們看到就是幾分鐘就這麼大,我們平時用的是 mp3 、aac 格式的,我們如何辦到的呢,這裡我們繼續看一下 mp3 格式如何能生成 。

PCM 轉化為 AAC 檔案格式

生成 aac 檔案播放

public class AACUtil {

    ...

    /**
     * 初始化AAC編碼器
     */

    private void initAACMediaEncode() {
        try {

            //引數對應-> mime type、取樣率、聲道數
            MediaFormat encodeFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 160001);
            encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, 64000);//位元率
            encodeFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
            encodeFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, AudioFormat.CHANNEL_IN_MONO);
            encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1024);//作用於inputBuffer的大小

            mediaEncode = MediaCodec.createEncoderByType(encodeType);
            mediaEncode.configure(encodeFormat, nullnull, MediaCodec.CONFIGURE_FLAG_ENCODE);
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (mediaEncode == null) {
            LogUtil.e("create mediaEncode failed");
            return;
        }

        mediaEncode.start();
        encodeInputBuffers = mediaEncode.getInputBuffers();
        encodeOutputBuffers = mediaEncode.getOutputBuffers();
        encodeBufferInfo = new MediaCodec.BufferInfo();
    }

    private boolean codeOver = false;

    /**
     * 開始轉碼
     * 音訊資料{@link #srcPath}先解碼成PCM  PCM資料在編碼成MediaFormat.MIMETYPE_AUDIO_AAC音訊格式
     * mp3->PCM->aac
     */

    public void startAsync() {
        LogUtil.w("start");
        new Thread(new DecodeRunnable()).start();
    }


    /**
     * 解碼{@link #srcPath}音訊檔案 得到PCM資料塊
     *
     * @return 是否解碼完所有資料
     */

    private void srcAudioFormatToPCM() {
        File file = new File(srcPath);// 指定要讀取的檔案
        FileInputStream fio = null;
        try {
            fio = new FileInputStream(file);
            byte[] bb = new byte[1024];
            while (!codeOver) {
                if (fio.read(bb) != -1) {
                    LogUtil.e("============   putPCMData ============" + bb.length);
                    dstAudioFormatFromPCM(bb);
                } else {
                    codeOver = true;
                }
            }

            fio.close();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }


    private byte[] chunkAudio = new byte[0];

    /**
     * 編碼PCM資料 得到AAC格式的音訊檔案
     */

    private void dstAudioFormatFromPCM(byte[] pcmData) {

        int inputIndex;
        ByteBuffer inputBuffer;
        int outputIndex;
        ByteBuffer outputBuffer;

        int outBitSize;
        int outPacketSize;
        byte[] PCMAudio;
        PCMAudio = pcmData;

        encodeInputBuffers = mediaEncode.getInputBuffers();
        encodeOutputBuffers = mediaEncode.getOutputBuffers();
        encodeBufferInfo = new MediaCodec.BufferInfo();


        inputIndex = mediaEncode.dequeueInputBuffer(0);
        inputBuffer = encodeInputBuffers[inputIndex];
        inputBuffer.clear();
        inputBuffer.limit(PCMAudio.length);
        inputBuffer.put(PCMAudio);//PCM資料填充給inputBuffer
        mediaEncode.queueInputBuffer(inputIndex, 0, PCMAudio.length, 00);//通知編碼器 編碼


        outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 0);

        while (outputIndex > 0) {

            outBitSize = encodeBufferInfo.size;
            outPacketSize = outBitSize + 7;//7為ADT頭部的大小
            outputBuffer = encodeOutputBuffers[outputIndex];//拿到輸出Buffer
            outputBuffer.position(encodeBufferInfo.offset);
            outputBuffer.limit(encodeBufferInfo.offset + outBitSize);
            chunkAudio = new byte[outPacketSize];
            addADTStoPacket(chunkAudio, outPacketSize);//新增ADTS
            outputBuffer.get(chunkAudio, 7, outBitSize);//將編碼得到的AAC資料 取出到byte[]中

            try {
                //錄製aac音訊檔案,儲存在手機記憶體中
                bos.write(chunkAudio, 0, chunkAudio.length);
                bos.flush();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }

            outputBuffer.position(encodeBufferInfo.offset);
            mediaEncode.releaseOutputBuffer(outputIndex, false);
            outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 0);

        }

    }

    /**
     * 新增ADTS頭
     *
     * @param packet
     * @param packetLen
     */

    private void addADTStoPacket(byte[] packet, int packetLen) {
        int profile = 2// AAC LC
        int freqIdx = 8// 16KHz
        int chanCfg = 1// CPE

        // fill in ADTS data
        packet[0] = (byte0xFF;
        packet[1] = (byte0xF1;
        packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
        packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
        packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
        packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
        packet[6] = (byte0xFC;

    }

    /**
     * 釋放資源
     */

    public void release() {
      ...
    }

    /**
     * 解碼執行緒
     */

    private class DecodeRunnable implements Runnable {

        @Override
        public void run() {
            srcAudioFormatToPCM();
        }
    }

}
複製程式碼

AudioStack 播放

AudioTrack 類可以完成Android平臺上音訊資料的輸出任務。AudioTrack有兩種資料載入模式(MODE_STREAM和MODE_STATIC),對應的是資料載入模式和音訊流型別, 對應著兩種完全不同的使用場景。

MODE_STREAM:在這種模式下,通過write一次次把音訊資料寫到AudioTrack中。這和平時通過write系統呼叫往檔案中寫資料類似,但這種工作方式每次都需要把資料從使用者提供的Buffer中拷貝到AudioTrack內部的Buffer中,這在一定程度上會使引入延時。為解決這一問題,AudioTrack就引入了第二種模式。

MODE_STATIC:這種模式下,在play之前只需要把所有資料通過一次write呼叫傳遞到AudioTrack中的內部緩衝區,後續就不必再傳遞資料了。這種模式適用於像鈴聲這種記憶體佔用量較小,延時要求較高的檔案。但它也有一個缺點,就是一次write的資料不能太多,否則系統無法分配足夠的記憶體來儲存全部資料。

播放聲音可以用MediaPlayer和AudioTrack,兩者都提供了Java API供應用開發者使用。雖然都可以播放聲音,但兩者還是有很大的區別的,其中最大的區別是MediaPlayer可以播放多種格式的聲音檔案,例如MP3,AAC,WAV,OGG,MIDI等。MediaPlayer會在framework層建立對應的音訊解碼器。而AudioTrack只能播放已經解碼的PCM流,如果對比支援的檔案格式的話則是AudioTrack只支援wav格式的音訊檔案,因為wav格式的音訊檔案大部分都是PCM流。AudioTrack不建立解碼器,所以只能播放不需要解碼的wav檔案。

3.1 音訊流的型別

在AudioTrack建構函式中,會接觸到AudioManager.STREAM_MUSIC這個引數。它的含義與Android系統對音訊流的管理和分類有關。

Android將系統的聲音分為好幾種流型別,下面是幾個常見的:

STREAM_ALARM:警告聲
STREAM_MUSIC:音樂聲,例如music等
STREAM_RING:鈴聲
STREAM_SYSTEM:系統聲音,例如低電提示音,鎖屏音等
STREAM_VOCIE_CALL:通話聲

注意:上面這些型別的劃分和音訊資料本身並沒有關係。例如MUSIC和RING型別都可以是某首MP3歌曲。另外,聲音流型別的選擇沒有固定的標準,例如,鈴聲預覽中的鈴聲可以設定為MUSIC型別。音訊流型別的劃分和Audio系統對音訊的管理策略有關。

3.2 Buffer分配和Frame的概念

在計算Buffer分配的大小的時候,我們經常用到的一個方法就是:getMinBufferSize。這個函式決定了應用層分配多大的資料Buffer。

AudioTrack.getMinBufferSize(8000,//每秒8K個取樣點                              
        AudioFormat.CHANNEL_CONFIGURATION_STEREO,//雙聲道                  
        AudioFormat.ENCODING_PCM_16BIT);
複製程式碼

從AudioTrack.getMinBufferSize開始追溯程式碼,可以發現在底層的程式碼中有一個很重要的概念:Frame(幀)。Frame是一個單位,用來描述資料量的多少。1單位的Frame等於1個取樣點的位元組數×聲道數(比如PCM16,雙聲道的1個Frame等於2×2=4位元組)。1個取樣點只針對一個聲道,而實際上可能會有一或多個聲道。由於不能用一個獨立的單位來表示全部聲道一次取樣的資料量,也就引出了Frame的概念。Frame的大小,就是一個取樣點的位元組數×聲道數。另外,在目前的音效卡驅動程式中,其內部緩衝區也是採用Frame作為單位來分配和管理的。

getMinBufSize會綜合考慮硬體的情況(諸如是否支援取樣率,硬體本身的延遲情況等)後,得出一個最小緩衝區的大小。一般我們分配的緩衝大小會是它的整數倍。

3.3 構建過程

每一個音訊流對應著一個AudioTrack類的一個例項,每個AudioTrack會在建立時註冊到 AudioFlinger中,由AudioFlinger把所有的AudioTrack進行混合(Mixer),然後輸送到AudioHardware中進行播放,目前Android同時最多可以建立32個音訊流,也就是說,Mixer最多會同時處理32個AudioTrack的資料流。

3.4 Show Me The Code

public class AudioTrackManager {
    ...
    //音訊流型別
    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;

    private void initData() {
        //根據取樣率,取樣精度,單雙聲道來得到frame的大小。
        mMinBufferSize = AudioTrack.getMinBufferSize(mSampleRateInHz, mChannelConfig, mAudioFormat);//計算最小緩衝區
        //注意,按照數字音訊的知識,這個算出來的是一秒鐘buffer的大小。
        //建立AudioTrack
        mAudioTrack = new AudioTrack(mStreamType, mSampleRateInHz, mChannelConfig,
                mAudioFormat, mMinBufferSize, mMode);
    }


    /**
     * 啟動播放執行緒
     */

    private void startThread() {
        destroyThread();
        isStart = true;
        if (mRecordThread == null) {
            mRecordThread = new Thread(recordRunnable);
            mRecordThread.start();
        }
    }

    /**
     * 播放執行緒
     */

    private 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
     */

    public void startPlay(String path) {
        try {
            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();
        }
    }

}
複製程式碼

原始碼地址

https://github.com/StudyLifeTime/basicvideotutorial



程式設計·思維·職場
歡迎掃碼關注

音視訊入門之音訊採集、編碼、播放

相關文章