溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

Android 音頻錄音與播放

發(fā)布時(shí)間:2020-07-08 02:38:41 來(lái)源:網(wǎng)絡(luò) 閱讀:1042 作者:Android丶VG 欄目:移動(dòng)開(kāi)發(fā)

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

介紹音頻的采集、編碼、生成文件、轉(zhuǎn)碼等操作,通過(guò) AudioRecord 采集音頻,生成三種格式的文件格式(pcm、wav、aac),用 AudioStack 來(lái)播放這個(gè)音頻。

Android 音頻錄音與播放

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

這里簡(jiǎn)單的介紹一下常見(jiàn)的三種音頻格式:

PCM :PCM(Pulse Code Modulation—-脈碼調(diào)制錄音)。所謂 PCM 錄音就是將聲音等模擬信號(hào)變成符號(hào)化的脈沖列,使用三個(gè)參數(shù)(聲道數(shù)、採(cǎi)樣位數(shù)和采樣頻率)來(lái)表示聲音。PCM 信號(hào)是就未經(jīng)過(guò)任何編碼和壓縮處理。與模擬信號(hào)比,它不易受傳送系統(tǒng)的雜波及失真的影響。動(dòng)態(tài)范圍寬,可得到音質(zhì)相當(dāng)好的影響效果。

WAV : WAV 是一種無(wú)損的音頻文件格式,WAV 符合 RIFF(Resource Interchange File Format) 規(guī)范。所有的 WAV 都有一個(gè)文件頭,這個(gè)文件頭音頻流的編碼參數(shù)。WAV 對(duì)音頻流的編碼沒(méi)有硬性規(guī)定,除了 PCM 之外,還有幾乎所有支持 ACM 規(guī)范的編碼都可以為 WAV 的音頻流進(jìn)行編碼。

簡(jiǎn)單來(lái)說(shuō):WAV 是一種無(wú)損的音頻文件格式,PCM是沒(méi)有壓縮的編碼方式

AAC : AAC(Advanced Audio Coding),中文稱(chēng)為“高級(jí)音頻編碼”,出現(xiàn)于 1997 年,基于 MPEG-2的音頻編碼技術(shù)。由 Fraunhofer IIS、杜比實(shí)驗(yàn)室、AT&T、Sony(索尼)等公司共同開(kāi)發(fā),目的是取代 MP3 格式。2000 年,MPEG-4 標(biāo)準(zhǔn)出現(xiàn)后,AAC 重新集成了其特性,加入了 SBR 技術(shù)和 PS 技術(shù),為了區(qū)別于傳統(tǒng)的 MPEG-2 AAC 又稱(chēng)為 MPEG-4 AAC。他是一種專(zhuān)為聲音數(shù)據(jù)設(shè)計(jì)的文件壓縮格式,與 MP3 類(lèi)似。利用 AAC 格式,可使聲音文件明顯減小,而不會(huì)讓人感覺(jué)聲音質(zhì)量有所降低 。

二、使用 AudioRecord 實(shí)現(xiàn)錄音生成 PCM 文件

AudioRecord 是 Android 系統(tǒng)提供的用于實(shí)現(xiàn)錄音的功能類(lèi),要想了解這個(gè)類(lèi)的具體的說(shuō)明和用法,可以去看一下官方的文檔,如參考鏈接。

AndioRecord 類(lèi)的主要功能是讓各種 Java 應(yīng)用能夠管理音頻資源,以便它們通過(guò)此類(lèi)能夠錄制聲音相關(guān)的硬件所收集的聲音。此功能的實(shí)現(xiàn)就是通過(guò) “ pulling ”(讀?。〢udioRecord 對(duì)象的聲音數(shù)據(jù)來(lái)完成的。在錄音過(guò)程中,應(yīng)用所需要做的就是通過(guò)后面三個(gè)類(lèi)方法中的一個(gè)去及時(shí)地獲取AudioRecord對(duì)象的錄音數(shù)據(jù). AudioRecord類(lèi)提供的三個(gè)獲取聲音數(shù)據(jù)的方法分別是:

  • read(byte[], int, int)
  • read(short[], int, int)
  • read(ByteBuffer, int)

無(wú)論選擇使用那一個(gè)方法都必須事先設(shè)定方便用戶(hù)的聲音數(shù)據(jù)的存儲(chǔ)格式。

開(kāi)始錄音的時(shí)候,AudioRecord 需要初始化一個(gè)相關(guān)聯(lián)的聲音 buffer, 這個(gè) buffer 主要是用來(lái)保存新的聲音數(shù)據(jù)。這個(gè) buffer 的大小,我們可以在對(duì)象構(gòu)造期間去指定。它表明一個(gè) AudioRecord 對(duì)象還沒(méi)有被讀?。ㄍ剑┞曇魯?shù)據(jù)前能錄多長(zhǎng)的音(即一次可以錄制的聲音容量)。聲音數(shù)據(jù)從音頻硬件中被讀出,數(shù)據(jù)大小不超過(guò)整個(gè)錄音數(shù)據(jù)的大?。梢苑侄啻巫x出),即每次讀取初始化 buffer 容量的數(shù)據(jù)。

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

主要是聲明一些用到的參數(shù),具體解釋可以看注釋。

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

private static final int mSampleRateInHz = 44100;
//指定捕獲音頻的聲道數(shù)目。在 AudioFormat 類(lèi)中指定用于此的常量,單聲道

private static final int mChannelConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO;
//指定音頻量化位數(shù) ,在 AudioFormat 類(lèi)中指定了以下各種可能的常量。通常我們選擇 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類(lèi)的getMinBufferSize方法可以獲得。
private int mBufferSizeInBytes;

// 聲明 AudioRecord 對(duì)象
private AudioRecord mAudioRecord = null;
2.2 獲取 buffer 的大小并創(chuàng)建 AudioRecord
//初始化數(shù)據(jù),計(jì)算最小緩沖區(qū)
mBufferSizeInBytes = AudioRecord.getMinBufferSize(mSampleRateInHz, mChannelConfig, mAudioFormat);
//創(chuàng)建AudioRecorder對(duì)象
mAudioRecord = new AudioRecord(mAudioSource, mSampleRateInHz, mChannelConfig,
                               mAudioFormat, mBufferSizeInBytes);
2.3 寫(xiě)文件
    @Override
    public void run() {
        //標(biāo)記為開(kāi)始采集狀態(tài)
        isRecording = true;
        //創(chuàng)建文件
        createFile();

        try {

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

            //最小緩沖區(qū)
            byte[] buffer = new byte[mBufferSizeInBytes];
            //獲取到文件的數(shù)據(jù)流
            mDataOutputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(mRecordingFile)));

            //開(kāi)始錄音
            mAudioRecord.startRecording();
            //getRecordingState獲取當(dāng)前AudioReroding是否正在采集數(shù)據(jù)的狀態(tài)
            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 權(quán)限申請(qǐng)

權(quán)限需求:WRITE_EXTERNAL_STORAGE、READ_EXTERNAL_STORAGE(部份手機(jī)必須要申請(qǐng)這個(gè)權(quán)限)、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 采集小結(jié)

到現(xiàn)在基本的錄音的流程就介紹完了,但是這時(shí)候問(wèn)題來(lái)了:

  • 我按照流程,把音頻數(shù)據(jù)都輸出到文件里面了,停止錄音后,打開(kāi)此文件,發(fā)現(xiàn)不能播放,到底是為什么呢?

按照流程走完了,數(shù)據(jù)是進(jìn)去了,但是現(xiàn)在的文件里面的內(nèi)容僅僅是最原始的音頻數(shù)據(jù),術(shù)語(yǔ)稱(chēng)為 RAW(中文解釋是“原材料”或“未經(jīng)處理的東西”),這時(shí)候,你讓播放器去打開(kāi),它既不知道保存的格式是什么,又不知道如何進(jìn)行解碼操作。當(dāng)然播放不了。

  • 那如何才能在播放器中播放我錄制的內(nèi)容呢?

在文件的數(shù)據(jù)開(kāi)頭加入AAC HEAD 或者 AAC 數(shù)據(jù)即可,也就是文件頭。只有加上文件頭部的數(shù)據(jù),播放器才能正確的知道里面的內(nèi)容到底是什么,進(jìn)而能夠正常的解析并播放里面的內(nèi)容。

三、PCM 轉(zhuǎn)化為 WAV

在文件的數(shù)據(jù)開(kāi)頭加入 WAVE HEAD 或者 AAC 數(shù)據(jù)即可,也就是文件頭。只有加上文件頭部的數(shù)據(jù),播放器才能正確的知道里面的內(nèi)容到底是什么,進(jìn)而能夠正常的解析并播放里面的內(nèi)容。具體的頭文件的描述,在 Play a WAV file on an AudioTrack 里面可以進(jìn)行了解。

public class WAVUtil {

    /**
     * PCM文件轉(zhuǎn)WAV文件
     *
     * @param inPcmFilePath  輸入PCM文件路徑
     * @param outWavFilePath 輸出WAV文件路徑
     * @param sampleRate     采樣率,例如44100
     * @param channels       聲道數(shù) 單聲道:1或雙聲道:2
     * @param bitNum         采樣位數(shù),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 {
            //采樣字節(jié)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 整個(gè)音頻PCM數(shù)據(jù)大小
     * @param totalDataLen  整個(gè)數(shù)據(jù)大小
     * @param sampleRate    采樣率
     * @param channels      聲道數(shù)
     * @param byteRate      采樣字節(jié)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);//數(shù)據(jù)大小
        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] = ' ';//過(guò)渡字節(jié)
        //數(shù)據(jù)大小
        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;
        //通道數(shù)
        header[22] = (byte) channels;
        header[23] = 0;
        //采樣率,每個(gè)通道的播放速度
        header[24] = (byte) (sampleRate & 0xff);
        header[25] = (byte) ((sampleRate >> 8) & 0xff);
        header[26] = (byte) ((sampleRate >> 16) & 0xff);
        header[27] = (byte) ((sampleRate >> 24) & 0xff);
        //音頻數(shù)據(jù)傳送速率,采樣率*通道數(shù)*采樣深度/8
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        // 確定系統(tǒng)一次要處理多少個(gè)這樣字節(jié)的數(shù)據(jù),確定緩沖區(qū),通道數(shù)*采樣位數(shù)
        header[32] = (byte) (channels * 16 / 8);
        header[33] = 0;
        //每個(gè)樣本的數(shù)據(jù)位數(shù)
        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);
    }
}

然后生成了相對(duì)的 WAV 文件,我們用用手機(jī)自帶播放器打開(kāi)此時(shí)就能正常播放,但是我們發(fā)現(xiàn)他的大小比較大,我們看到就是幾分鐘就這么大,我們平時(shí)用的是 mp3 、aac 格式的,我們?nèi)绾无k到的呢?

四、PCM 轉(zhuǎn)化為 AAC 文件格式

生成 aac 文件播放

public class AACUtil {

    ...

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

            //參數(shù)對(duì)應(yīng)-> mime type、采樣率、聲道數(shù)
            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;

    /**
     * 開(kāi)始轉(zhuǎn)碼
     * 音頻數(shù)據(jù){@link #srcPath}先解碼成PCM  PCM數(shù)據(jù)在編碼成MediaFormat.MIMETYPE_AUDIO_AAC音頻格式
     * mp3->PCM->aac
     */
    public void startAsync() {
        LogUtil.w("start");
        new Thread(new DecodeRunnable()).start();
    }

    /**
     * 解碼{@link #srcPath}音頻文件 得到PCM數(shù)據(jù)塊
     *
     * @return 是否解碼完所有數(shù)據(jù)
     */
    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數(shù)據(jù) 得到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數(shù)據(jù)填充給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數(shù)據(jù) 取出到byte[]中

            try {
                //錄制aac音頻文件,保存在手機(jī)內(nèi)存中
                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() {
      ...
    }

    /**
     * 解碼線(xiàn)程
     */
    private class DecodeRunnable implements Runnable {

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

}

五、AudioStack 播放

AudioTrack 類(lèi)可以完成 Android 平臺(tái)上音頻數(shù)據(jù)的輸出任務(wù)。AudioTrack 有兩種數(shù)據(jù)加載模式(MODE_STREAM 和 MODE_STATIC),對(duì)應(yīng)的是數(shù)據(jù)加載模式和音頻流類(lèi)型, 對(duì)應(yīng)著兩種完全不同的使用場(chǎng)景。

  • MODE_STREAM:在這種模式下,通過(guò) write 一次次把音頻數(shù)據(jù)寫(xiě)到 AudioTrack 中。這和平時(shí)通過(guò) write 系統(tǒng)調(diào)用往文件中寫(xiě)數(shù)據(jù)類(lèi)似,但這種工作方式每次都需要把數(shù)據(jù)從用戶(hù)提供的 Buffer 中拷貝到 AudioTrack 內(nèi)部的 Buffer 中,這在一定程度上會(huì)使引入延時(shí)。為解決這一問(wèn)題,AudioTrack 就引入了第二種模式。
  • MODE_STATIC:這種模式下,在 play 之前只需要把所有數(shù)據(jù)通過(guò)一次 write 調(diào)用傳遞到 AudioTrack 中的內(nèi)部緩沖區(qū),后續(xù)就不必再傳遞數(shù)據(jù)了。這種模式適用于像鈴聲這種內(nèi)存占用量較小,延時(shí)要求較高的文件。但它也有一個(gè)缺點(diǎn),就是一次write的數(shù)據(jù)不能太多,否則系統(tǒng)無(wú)法分配足夠的內(nèi)存來(lái)存儲(chǔ)全部數(shù)據(jù)。

播放聲音可以用 MediaPlayer 和 AudioTrack,兩者都提供了 Java API 供應(yīng)用開(kāi)發(fā)者使用。雖然都可以播放聲音,但兩者還是有很大的區(qū)別的,其中最大的區(qū)別是MediaPlayer 可以播放多種格式的聲音文件,例如 MP3,AAC,WAV,OGG,MIDI 等。MediaPlayer 會(huì)在 framework 層創(chuàng)建對(duì)應(yīng)的音頻解碼器。而 AudioTrack 只能播放已經(jīng)解碼的 PCM 流,如果對(duì)比支持的文件格式的話(huà)則是 AudioTrack 只支持 wav 格式的音頻文件,因?yàn)?wav 格式的音頻文件大部分都是 PCM 流。AudioTrack 不創(chuàng)建解碼器,所以只能播放不需要解碼的 wav 文件。

5.1 音頻流的類(lèi)型

在 AudioTrack 構(gòu)造函數(shù)中,會(huì)接觸到 AudioManager.STREAM_MUSIC 這個(gè)參數(shù)。它的含義與 Android 系統(tǒng)對(duì)音頻流的管理和分類(lèi)有關(guān)。

Android 將系統(tǒng)的聲音分為好幾種流類(lèi)型,下面是幾個(gè)常見(jiàn)的:

STREAM_ALARM:警告聲
STREAM_MUSIC:音樂(lè)聲,例如 music 等
STREAM_RING:鈴聲
STREAM_SYSTEM:系統(tǒng)聲音,例如低電提示音,鎖屏音等
STREAM_VOCIE_CALL:通話(huà)聲

注意:上面這些類(lèi)型的劃分和音頻數(shù)據(jù)本身并沒(méi)有關(guān)系。例如 MUSIC 和 RING 類(lèi)型都可以是某首 MP3 歌曲。另外,聲音流類(lèi)型的選擇沒(méi)有固定的標(biāo)準(zhǔn),例如,鈴聲預(yù)覽中的鈴聲可以設(shè)置為MUSIC類(lèi)型。音頻流類(lèi)型的劃分和Audio系統(tǒng)對(duì)音頻的管理策略有關(guān)。

5.2 Buffer 分配和 Frame 的概念

在計(jì)算 Buffer 分配的大小的時(shí)候,我們經(jīng)常用到的一個(gè)方法就是:getMinBufferSize。這個(gè)函數(shù)決定了應(yīng)用層分配多大的數(shù)據(jù) Buffer。

AudioTrack.getMinBufferSize(8000,//每秒8K個(gè)采樣點(diǎn)
         AudioFormat.CHANNEL_CONFIGURATION_STEREO,//雙聲道
           AudioFormat.ENCODING_PCM_16BIT);

從 AudioTrack.getMinBufferSize 開(kāi)始追溯代碼,可以發(fā)現(xiàn)在底層的代碼中有一個(gè)很重要的概念:Frame(幀)。Frame 是一個(gè)單位,用來(lái)描述數(shù)據(jù)量的多少。1 單位的 Frame 等于 1 個(gè)采樣點(diǎn)的字節(jié)數(shù) × 聲道數(shù)(比如 PCM16,雙聲道的 1 個(gè) Frame 等于 2×2=4 字節(jié))。1 個(gè)采樣點(diǎn)只針對(duì)一個(gè)聲道,而實(shí)際上可能會(huì)有一或多個(gè)聲道。由于不能用一個(gè)獨(dú)立的單位來(lái)表示全部聲道一次采樣的數(shù)據(jù)量,也就引出了 Frame 的概念。Frame 的大小,就是一個(gè)采樣點(diǎn)的字節(jié)數(shù) × 聲道數(shù)。另外,在目前的聲卡驅(qū)動(dòng)程序中,其內(nèi)部緩沖區(qū)也是采用 Frame 作為單位來(lái)分配和管理的。

getMinBufSize 會(huì)綜合考慮硬件的情況(諸如是否支持采樣率,硬件本身的延遲情況等)后,得出一個(gè)最小緩沖區(qū)的大小。一般我們分配的緩沖大小會(huì)是它的整數(shù)倍。

5.3 構(gòu)建過(guò)程

每一個(gè)音頻流對(duì)應(yīng)著一個(gè) AudioTrack 類(lèi)的一個(gè)實(shí)例,每個(gè) AudioTrack 會(huì)在創(chuàng)建時(shí)注冊(cè)到 AudioFlinger 中,由 AudioFlinger 把所有的 AudioTrack 進(jìn)行混合(Mixer),然后輸送到 AudioHardware 中進(jìn)行播放,目前 Android 同時(shí)最多可以創(chuàng)建 32 個(gè)音頻流,也就是說(shuō),Mixer 最多會(huì)同時(shí)處理 32 個(gè) AudioTrack 的數(shù)據(jù)流。

public class AudioTrackManager {
    ...
    //音頻流類(lèi)型
    private static final int mStreamType = AudioManager.STREAM_MUSIC;
    //指定采樣率 (MediaRecoder 的采樣率通常是8000Hz AAC的通常是44100Hz。 設(shè)置采樣率為44100,目前為常用的采樣率,官方文檔表示這個(gè)值可以兼容所有的設(shè)置)
    private static final int mSampleRateInHz = 44100;
    //指定捕獲音頻的聲道數(shù)目。在AudioFormat類(lèi)中指定用于此的常量
    private static final int mChannelConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO; //單聲道
    //指定音頻量化位數(shù) ,在AudioFormaat類(lèi)中指定了以下各種可能的常量。通常我們選擇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類(lèi)的getMinBufferSize方法可以獲得。
    private int mMinBufferSize;
    //STREAM的意思是由用戶(hù)在應(yīng)用程序通過(guò)write方式把數(shù)據(jù)一次一次得寫(xiě)到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;

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

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

    /**
     * 播放線(xiàn)程
     */
    private Runnable recordRunnable = new Runnable() {
        @Override
        public void run() {
            try {
                //設(shè)置線(xiàn)程的優(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;
                    }
                    //一邊播放一邊寫(xiě)入語(yǔ)音數(shù)據(jù)
                    if (readCount != 0 && readCount != -1) {
                        //判斷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();
            }
        }

    };

    /**
     * 啟動(dòng)播放
     *
     * @param path
     */
    public void startPlay(String path) {
        try {
            setPath(path);
            startThread();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 停止播放
     */
    public void stopPlay() {
        try {
            destroyThread();//銷(xiāo)毀線(xiàn)程
            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();
        }
    }

}
向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI