Android 音頻錄音與播放

原文鏈接:https://www.jianshu.com/p/5966ed7c5baf

介紹音頻的採集、編碼、生成文件、轉碼等操作,通過 AudioRecord 採集音頻,生成三種格式的文件格式(pcm、wav、aac),用 AudioStack 來播放這個音頻。

Android 音頻錄音與播放

一、PCM 、WAV、AAC 的文件頭介紹

這裏簡單的介紹一下常見的三種音頻格式:

PCM :PCM(Pulse Code Modulation—-脈碼調製錄音)。所謂 PCM 錄音就是將聲音等模擬信號變成符號化的脈衝列,使用三個參數(聲道數、採樣位數和採樣頻率)來表示聲音。PCM 信號是就未經過任何編碼和壓縮處理。與模擬信號比,它不易受傳送系統的雜波及失真的影響。動態範圍寬,可得到音質相當好的影響效果。

WAV : WAV 是一種無損的音頻文件格式,WAV 符合 RIFF(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 格式,可使聲音文件明顯減小,而不會讓人感覺聲音質量有所降低 。

二、使用 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 容量的數據。

2.1 首先要聲明一些全局的變量和常量參數

主要是聲明一些用到的參數,具體解釋可以看註釋。

/指定音頻源 這個和MediaRecorder是相同的 MediaRecorder.AudioSource.MIC指的是麥克風
private static final int mAudioSource = MediaRecorder.AudioSource.MIC;
//指定採樣率 (MediaRecoder 的採樣率通常是8000Hz,16000Hz
//AAC的通常是 44100Hz。 設置採樣率爲 44100,目前爲常用的採樣率,官方文檔表示這個值可以兼容所有的設置)

private static final int mSampleRateInHz = 44100;
//指定捕獲音頻的聲道數目。在 AudioFormat 類中指定用於此的常量,單聲道

private static final int mChannelConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO;
//指定音頻量化位數 ,在 AudioFormat 類中指定了以下各種可能的常量。通常我們選擇 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;

2.2 獲取 buffer 的大小並創建 AudioRecord

//初始化數據,計算最小緩衝區
mBufferSizeInBytes = AudioRecord.getMinBufferSize(mSampleRateInHz, mChannelConfig, mAudioFormat);
//創建AudioRecorder對象
mAudioRecord = new AudioRecord(mAudioSource, mSampleRateInHz, mChannelConfig,
                               mAudioFormat, mBufferSizeInBytes);

2.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);
        }
    }

2.4 權限申請

權限需求:WRITE_EXTERNAL_STORAGE、READ_EXTERNAL_STORAGE(部份手機必須要申請這個權限)、RECORD_AUDIO

    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

2.5 採集小結

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

  • 我按照流程,把音頻數據都輸出到文件裏面了,停止錄音後,打開此文件,發現不能播放,到底是爲什麼呢?

按照流程走完了,數據是進去了,但是現在的文件裏面的內容僅僅是最原始的音頻數據,術語稱爲 RAW(中文解釋是“原材料”或“未經處理的東西”),這時候,你讓播放器去打開,它既不知道保存的格式是什麼,又不知道如何進行解碼操作。當然播放不了。

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

在文件的數據開頭加入AAC HEAD 或者 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 out, long 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, 0, 44);
    }
}

然後生成了相對的 WAV 文件,我們用用手機自帶播放器打開此時就能正常播放,但是我們發現他的大小比較大,我們看到就是幾分鐘就這麼大,我們平時用的是 mp3 、aac 格式的,我們如何辦到的呢?

四、PCM 轉化爲 AAC 文件格式

生成 aac 文件播放

public class AACUtil {

    ...

    /**
     * 初始化AAC編碼器
     */
    private void initAACMediaEncode() {
        try {

            //參數對應-> mime type、採樣率、聲道數
            MediaFormat encodeFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 16000, 1);
            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, null, null, 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, 0, 0);//通知編碼器 編碼

        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] = (byte) 0xFF;
        packet[1] = (byte) 0xF1;
        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] = (byte) 0xFC;

    }

    /**
     * 釋放資源
     */
    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 文件。

5.1 音頻流的類型

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

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

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

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

5.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 會綜合考慮硬件的情況(諸如是否支持採樣率,硬件本身的延遲情況等)後,得出一個最小緩衝區的大小。一般我們分配的緩衝大小會是它的整數倍。

5.3 構建過程

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

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

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章