1. 程式人生 > >Android音訊開發之使用AudioRecord錄製

Android音訊開發之使用AudioRecord錄製

本文主要是記錄Android端音訊開發

本例記錄使用AudioRecord 錄製音訊,播放使用AudioTrack,儲存的檔案為pcm

只是簡單的測試用例,介面同上文

注意新增許可權

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
1 . AudioRecord 的工作流程:
--- 配置引數,初始化內部的音訊緩衝區

--- 開始採集
--- 需要一個執行緒,不斷地從 AudioRecord 的緩衝區將音訊資料“讀”出來,注意,這個過程一定要及時,否則就會出現“overrun”的錯誤,該錯誤在音訊開發中比較常見,意味著應用層沒有及時地“取走”音訊資料,導致內部的音訊緩衝區溢位。
---  停止採集,釋放資源

引數配置

public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes) throws IllegalArgumentException 
--- audioSource
該引數指的是音訊採集的輸入源,可選的值以常量的形式定義在 MediaRecorder.AudioSource 類中,常用的值包括:DEFAULT(預設),VOICE_RECOGNITION(用於語音識別,等同於DEFAULT),MIC(由手機麥克風輸入),VOICE_COMMUNICATION(用於VoIP應用)等等。

--- sampleRateInHz
取樣率,注意,目前44100Hz是唯一可以保證相容所有Android手機的取樣率。

--- channelConfig
通道數的配置,可選的值以常量的形式定義在 AudioFormat 類中,常用的是 CHANNEL_IN_MONO(單通道),CHANNEL_IN_STEREO(雙通道)

--- audioFormat
這個引數是用來配置“資料位寬”的,可選的值也是以常量的形式定義在 AudioFormat 類中,常用的是 ENCODING_PCM_16BIT(16bit),ENCODING_PCM_8BIT(8bit),注意,前者是可以保證相容所有Android手機的。

--- bufferSizeInBytes
這個是最難理解又最重要的一個引數,它配置的是 AudioRecord 內部的音訊緩衝區的大小,該緩衝區的值不能低於一幀“音訊幀”(Frame)的大小,一幀音訊幀的大小計算如下:
int size = 取樣率 x 位寬 x 取樣時間 x 通道數

取樣時間一般取 2.5ms~120ms 之間,由廠商或者具體的應用決定,我們其實可以推斷,每一幀的取樣時間取得越短,產生的延時就應該會越小,當然,碎片化的資料也就會越多。

在Android開發中,AudioRecord 類提供了一個幫助你確定這個 bufferSizeInBytes 的函式,原型如下:

int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat);

強烈建議由該函式計算出需要傳入的 bufferSizeInBytes,而不是自己手動計算。

當建立好了 AudioRecord 物件之後,就可以開始進行音訊資料的採集了,通過下面兩個函式控制採集的開始/停止:
AudioRecord.startRecording();
AudioRecord.stop();

呼叫的讀取資料的介面是:
AudioRecord.read(byte[] audioData, int offsetInBytes, int sizeInBytes);


2 . AudioTrack 的工作流程:

--- 配置引數,初始化內部的音訊播放緩衝區
---  開始播放
---  需要一個執行緒,不斷地向 AudioTrack 的緩衝區“寫入”音訊資料
---  停止播放,釋放資源

引數設定

public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode) throws IllegalArgumentException
--- streamType
這個引數代表著當前應用使用的哪一種音訊管理策略,當系統有多個程序需要播放音訊時,這個管理策略會決定最終的展現效果,該引數的可選的值以常量的形式定義在 AudioManager 類中,主要包括:

STREAM_VOCIE_CALL:電話聲音
STREAM_SYSTEM:系統聲音
STREAM_RING:鈴聲
STREAM_MUSCI:音樂聲
STREAM_ALARM:警告聲
STREAM_NOTIFICATION:通知聲

--- sampleRateInHz
取樣率,從AudioTrack原始碼的“audioParamCheck”函式可以看到,這個取樣率的取值範圍必須在 4000Hz~192000Hz 之間。

--- channelConfig
通道數的配置,可選的值以常量的形式定義在 AudioFormat 類中,常用的是 CHANNEL_IN_MONO(單通道),CHANNEL_IN_STEREO(雙通道)

--- audioFormat
這個引數是用來配置“資料位寬”的,可選的值也是以常量的形式定義在 AudioFormat 類中,常用的是 ENCODING_PCM_16BIT(16bit),ENCODING_PCM_8BIT(8bit),注意,前者是可以保證相容所有Android手機的。

--- bufferSizeInBytes
配置的是 AudioTrack 內部的音訊緩衝區的大小,該緩衝區的值不能低於一幀“音訊幀”(Frame)的大小,一幀音訊幀的大小計算如下:

int size = 取樣率 x 位寬 x 取樣時間 x 通道數

AudioTrack 類提供了一個幫助你確定這個 bufferSizeInBytes 的函式,原型如下:

int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat);

--- mode
AudioTrack 提供了兩種播放模式,一種是 static 方式,一種是 streaming 方式,前者需要一次性將所有的資料都寫入播放緩衝區,簡單高效,通常用於播放鈴聲、系統提醒的音訊片段; 後者則是按照一定的時間間隔不間斷地寫入音訊資料,理論上它可用於任何音訊播放的場景。

在 AudioTrack 類中,一個是 MODE_STATIC,另一個是 MODE_STREAM

import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.AsyncTask;
import android.os.Environment;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import com.cl.slack.mediarecorder.R;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import static android.Manifest.permission.RECORD_AUDIO;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;

public class AudioRecorderActivity extends AppCompatActivity
        implements View.OnClickListener {

    private final int REQ_PERMISSION_AUDIO = 0x01;
    private Button mRecode, mPlay;
    private File mAudioFile = null;
    private Thread mCaptureThread = null;
    private boolean mIsRecording,mIsPlaying;
    private int mFrequence = 44100;
    private int mChannelConfig = AudioFormat.CHANNEL_IN_MONO;
    private int mPlayChannelConfig = AudioFormat.CHANNEL_IN_STEREO;
    private int mAudioEncoding = AudioFormat.ENCODING_PCM_16BIT;

    private PlayTask mPlayer;
    private RecordTask mRecorder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_media_recorder_audio);

        mRecode = (Button) findViewById(R.id.audio_recode);
        mPlay = (Button) findViewById(R.id.audio_paly);

        mRecode.setText("recode");
        mPlay.setText("play");

        mPlay.setEnabled(false);

        mRecode.setOnClickListener(this);
        mPlay.setOnClickListener(this);

//        mRecorder = new RecordTask();
//        mPlayer = new PlayTask();


    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.audio_recode:
                if (mRecode.getTag() == null) {
                    startAudioRecode();
                } else {
                    stopAudioRecode();
                }
                break;
            case R.id.audio_paly:
                if (mPlay.getTag() == null) {
                    startAudioPlay();
                } else {
                    stopAudioPlay();
                }
                break;
        }
    }

    private void startAudioRecode() {
        if (checkPermission()) {
            PackageManager packageManager = this.getPackageManager();
            if (!packageManager.hasSystemFeature(PackageManager.FEATURE_MICROPHONE)) {
                showToast("This device doesn't have a mic!");
                return;
            }
            mRecode.setTag(this);
            mRecode.setText("stop");
            mPlay.setEnabled(false);

            File fpath = new File(Environment.getExternalStorageDirectory()
                    .getAbsolutePath() + "/slack");
            fpath.mkdirs();// 建立資料夾
            try {
                // 建立臨時檔案,注意這裡的格式為.pcm
                mAudioFile = File.createTempFile("recording", ".pcm", fpath);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            mRecorder = new RecordTask();
            mRecorder.execute();

            showToast("Recording started");

        } else {
            requestPermission();
        }
    }

    private void stopAudioRecode() {

        mIsRecording = false;

        mRecode.setTag(null);
        mRecode.setText("recode");
        mPlay.setEnabled(true);
        showToast("Recording Completed");
    }

    private void startAudioPlay() {
        mPlay.setTag(this);
        mPlay.setText("stop");

        mPlayer = new PlayTask();
        mPlayer.execute();

        showToast("Recording Playing");
    }

    private void stopAudioPlay() {

        mIsPlaying = false;

        mPlay.setTag(null);
        mPlay.setText("play");

    }


    private boolean checkPermission() {
        int result = ContextCompat.checkSelfPermission(getApplicationContext(),
                WRITE_EXTERNAL_STORAGE);
        int result1 = ContextCompat.checkSelfPermission(getApplicationContext(),
                RECORD_AUDIO);
        return result == PackageManager.PERMISSION_GRANTED &&
                result1 == PackageManager.PERMISSION_GRANTED;
    }

    private void requestPermission() {
        ActivityCompat.requestPermissions(this, new
                String[]{WRITE_EXTERNAL_STORAGE, RECORD_AUDIO}, REQ_PERMISSION_AUDIO);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           String permissions[], int[] grantResults) {
        switch (requestCode) {
            case REQ_PERMISSION_AUDIO:
                if (grantResults.length > 0) {
                    boolean StoragePermission = grantResults[0] ==
                            PackageManager.PERMISSION_GRANTED;
                    boolean RecordPermission = grantResults[1] ==
                            PackageManager.PERMISSION_GRANTED;

                    if (StoragePermission && RecordPermission) {
                        showToast("Permission Granted");
                    } else {
                        showToast("Permission  Denied");
                    }
                }
                break;
        }
    }

    private void showToast(String message) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show();
    }


    class RecordTask extends AsyncTask<Void,Integer,Void> {
        @Override
        protected Void doInBackground(Void... arg0) {
            mIsRecording = true;
            try {
                // 開通輸出流到指定的檔案
                DataOutputStream dos = new DataOutputStream(
                        new BufferedOutputStream(
                                new FileOutputStream(mAudioFile)));
                // 根據定義好的幾個配置,來獲取合適的緩衝大小
                int bufferSize = AudioRecord.getMinBufferSize(mFrequence,
                        mChannelConfig, mAudioEncoding);
                // 例項化AudioRecord
                AudioRecord record = new AudioRecord(
                        MediaRecorder.AudioSource.MIC, mFrequence,
                        mChannelConfig, mAudioEncoding, bufferSize);
                // 定義緩衝
                short[] buffer = new short[bufferSize];


                // 開始錄製
                record.startRecording();


                int r = 0; // 儲存錄製進度
                // 定義迴圈,根據isRecording的值來判斷是否繼續錄製
                while (mIsRecording) {
                    // 從bufferSize中讀取位元組,返回讀取的short個數
                    int bufferReadResult = record
                            .read(buffer, 0, buffer.length);
                    // 迴圈將buffer中的音訊資料寫入到OutputStream中
                    for (int i = 0; i < bufferReadResult; i++) {
                        dos.writeShort(buffer[i]);
                    }
                    publishProgress(new Integer(r)); // 向UI執行緒報告當前進度
                    r++; // 自增進度值
                }
                // 錄製結束
                record.stop();
                Log.i("slack", "::" + mAudioFile.length());
                dos.close();
            } catch (Exception e) {
                // TODO: handle exception
                Log.e("slack", "::" + e.getMessage());
            }
            return null;
        }


        // 當在上面方法中呼叫publishProgress時,該方法觸發,該方法在UI執行緒中被執行
        protected void onProgressUpdate(Integer... progress) {
            //
        }


        protected void onPostExecute(Void result) {

        }

    }

    /**
     * AudioTrack
     */
    class PlayTask extends AsyncTask<Void,Void,Void> {
        @Override
        protected Void doInBackground(Void... arg0) {
            mIsPlaying = true;
            int bufferSize = AudioTrack.getMinBufferSize(mFrequence,
                    mPlayChannelConfig, mAudioEncoding);
            short[] buffer = new short[bufferSize ];
            try {
                // 定義輸入流,將音訊寫入到AudioTrack類中,實現播放
                DataInputStream dis = new DataInputStream(
                        new BufferedInputStream(new FileInputStream(mAudioFile)));
                // 例項AudioTrack
                AudioTrack track = new AudioTrack(AudioManager.STREAM_MUSIC,
                        mFrequence,
                        mPlayChannelConfig, mAudioEncoding, bufferSize,
                        AudioTrack.MODE_STREAM);
                // 開始播放
                track.play();
                // 由於AudioTrack播放的是流,所以,我們需要一邊播放一邊讀取
                while (mIsPlaying && dis.available() > 0) {
                    int i = 0;
                    while (dis.available() > 0 && i < buffer.length) {
                        buffer[i] = dis.readShort();
                        i++;
                    }
                    // 然後將資料寫入到AudioTrack中
                    track.write(buffer, 0, buffer.length);
                }


                // 播放結束
                track.stop();
                dis.close();
            } catch (Exception e) {
                // TODO: handle exception
                Log.e("slack","error:" + e.getMessage());
            }
            return null;
        }


        protected void onPostExecute(Void result) {

        }


        protected void onPreExecute() {

        }
    }
}