前言
Android SDK 提供了兩套音頻采集的API,分別是:MediaRecorder 和 AudioRecord,前者是一個更加上層一點的API,它可以直接把手機麥克風錄入的音頻數據進行編碼壓縮(如AMR、MP3等)並存成文件,而后者則更接近底層,能夠更加自由靈活地控制,可以得到原始的一幀幀PCM音頻數據。
實現流程
- 獲取權限
- 初始化獲取每一幀流的Size
- 初始化音頻錄制AudioRecord
- 開始錄制與保存錄制音頻文件
- 停止錄制
- 給音頻文件添加頭部信息,並且轉換格式成wav
- 釋放AudioRecord,錄制流程完畢
獲取權限
<!--音頻錄制權限 --> <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" />
如果是Android5.0以上,以上3個權限需要動態授權
初始化獲取每一幀流的Size
private Integer mRecordBufferSize; private void initMinBufferSize(){ //獲取每一幀的字節流大小 mRecordBufferSize = AudioRecord.getMinBufferSize(8000 , AudioFormat.CHANNEL_IN_MONO , AudioFormat.ENCODING_PCM_16BIT); }
第一個參數sampleRateInHz 采樣率(赫茲),方法注釋里有說明
只能在4000到192000的范圍內取值
在AudioFormat類里
public static final int SAMPLE_RATE_HZ_MIN = 4000; 最小4000
public static final int SAMPLE_RATE_HZ_MAX = 192000; 最大192000
第二個參數channelConfig 聲道配置 描述音頻聲道的配置,例如左聲道/右聲道/前聲道/后聲道。
在AudioFormat類錄
public static final int CHANNEL_IN_LEFT = 0x4;//左聲道
public static final int CHANNEL_IN_RIGHT = 0x8;//右聲道
public static final int CHANNEL_IN_FRONT = 0x10;//前聲道
public static final int CHANNEL_IN_BACK = 0x20;//后聲道
public static final int CHANNEL_IN_LEFT_PROCESSED = 0x40;
public static final int CHANNEL_IN_RIGHT_PROCESSED = 0x80;
public static final int CHANNEL_IN_FRONT_PROCESSED = 0x100;
public static final int CHANNEL_IN_BACK_PROCESSED = 0x200;
public static final int CHANNEL_IN_PRESSURE = 0x400;
public static final int CHANNEL_IN_X_AXIS = 0x800;
public static final int CHANNEL_IN_Y_AXIS = 0x1000;
public static final int CHANNEL_IN_Z_AXIS = 0x2000;
public static final int CHANNEL_IN_VOICE_UPLINK = 0x4000;
public static final int CHANNEL_IN_VOICE_DNLINK = 0x8000;
public static final int CHANNEL_IN_MONO = CHANNEL_IN_FRONT;//單聲道
public static final int CHANNEL_IN_STEREO = (CHANNEL_IN_LEFT | CHANNEL_IN_RIGHT);//立體聲道(左右聲道)
第三個參數audioFormat 音頻格式 表示音頻數據的格式。
注意!一般的手機設備可能只支持 16位PCM編碼,如果其他的都會報錯為壞值.
public static final int ENCODING_PCM_16BIT = 2; //16位PCM編碼
public static final int ENCODING_PCM_8BIT = 3; //8位PCM編碼
public static final int ENCODING_PCM_FLOAT = 4; //4位PCM編碼
public static final int ENCODING_AC3 = 5;
public static final int ENCODING_E_AC3 = 6;
public static final int ENCODING_DTS = 7;
public static final int ENCODING_DTS_HD = 8;
public static final int ENCODING_MP3 = 9; //MP3編碼 此格式可能會因為不設備不支持報錯
public static final int ENCODING_AAC_LC = 10;
public static final int ENCODING_AAC_HE_V1 = 11;
public static final int ENCODING_AAC_HE_V2 = 12;
初始化音頻錄制AudioRecord
private AudioRecord mAudioRecord; private void initAudioRecord(){ mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC , 8000 , AudioFormat.CHANNEL_IN_MONO , AudioFormat.ENCODING_PCM_16BIT , mRecordBufferSize); }
- 第一個參數audioSource 音頻源 這里選擇使用麥克風:MediaRecorder.AudioSource.MIC
- 第二個參數sampleRateInHz 采樣率(赫茲) 與前面初始化獲取每一幀流的Size保持一致
- 第三個參數channelConfig 聲道配置 描述音頻聲道的配置,例如左聲道/右聲道/前聲道/后聲道。 與前面初始化獲取每一幀流的Size保持一致
- 第四個參數audioFormat 音頻格式 表示音頻數據的格式。 與前面初始化獲取每一幀流的Size保持一致
- 第五個參數緩存區大小,就是上面我們配置的AudioRecord.getMinBufferSize
開始錄制與保存錄制音頻文件
private boolean mWhetherRecord; private File pcmFile; private void startRecord(){ pcmFile = new File(AudioRecordActivity.this.getExternalCacheDir().getPath(),"audioRecord.pcm"); mWhetherRecord = true; new Thread(new Runnable() { @Override public void run() { mAudioRecord.startRecording();//開始錄制 FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(pcmFile); byte[] bytes = new byte[mRecordBufferSize]; while (mWhetherRecord){ mAudioRecord.read(bytes, 0, bytes.length);//讀取流 fileOutputStream.write(bytes); fileOutputStream.flush(); } Log.e(TAG, "run: 暫停錄制" ); mAudioRecord.stop();//停止錄制 fileOutputStream.flush(); fileOutputStream.close(); addHeadData();//添加音頻頭部信息並且轉成wav格式 } catch (FileNotFoundException e) { e.printStackTrace(); mAudioRecord.stop(); } catch (IOException e) { e.printStackTrace(); } } }).start(); }
這里說明一下為什么用布爾值,來關閉錄制.有些小伙伴會發現AudioRecord是可以獲取到錄制狀態的.那么肯定有人會用狀態來判斷while是否還需要處理流.這種是錯誤的做法.因為MIC屬於硬件層任何硬件的東西都是異步的而且會有很大的延時.所以回調的狀態也是有延時的,有時候流沒了,但是狀態還是顯示為正在錄制.
停止錄制
就是調用mAudioRecord.stop();方法來停止錄制,但是因為我在上面的保存流后做了調用停止視頻錄制,所以我這里只需要切換布爾值就可以關閉音頻錄制
private void stopRecord(){ mWhetherRecord = false; }
給音頻文件添加頭部信息,並且轉換格式成wav
音頻錄制完成后,這個時候去存儲目錄找到音頻文件部分,會提示無法播放文件.其實是因為沒有加入音頻頭部信息.一般通過麥克風采集的錄音數據都是PCM格式的,即不包含頭部信息,播放器無法知道音頻采樣率、位寬等參數,導致無法播放,顯然是非常不方便的。pcm轉換成wav,我們只需要在pcm的文件起始位置加上至少44個字節的WAV頭信息即可。
private void addHeadData(){ pcmFile = new File(AudioRecordActivity.this.getExternalCacheDir().getPath(),"audioRecord.pcm"); handlerWavFile = new File(AudioRecordActivity.this.getExternalCacheDir().getPath(),"audioRecord_handler.wav"); PcmToWavUtil pcmToWavUtil = new PcmToWavUtil(8000,AudioFormat.CHANNEL_IN_MONO,AudioFormat.ENCODING_PCM_16BIT); pcmToWavUtil.pcmToWav(pcmFile.toString(),handlerWavFile.toString()); }
寫入頭部信息的工具類
注意輸入File和輸出File不能同一個,因為沒有做緩存.
public class PcmToWavUtil { private static final String TAG = "PcmToWavUtil"; /** * 緩存的音頻大小 */ private int mBufferSize; /** * 采樣率 */ private int mSampleRate; /** * 聲道數 */ private int mChannel; /** * @param sampleRate sample rate、采樣率 * @param channel channel、聲道 * @param encoding Audio data format、音頻格式 */ PcmToWavUtil(int sampleRate, int channel, int encoding) { this.mSampleRate = sampleRate; this.mChannel = channel; this.mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, encoding); } /** * pcm文件轉wav文件 * * @param inFilename 源文件路徑 * @param outFilename 目標文件路徑 */ public void pcmToWav(String inFilename, String outFilename) { FileInputStream in; FileOutputStream out; long totalAudioLen;//總錄音長度 long totalDataLen;//總數據長度 long longSampleRate = mSampleRate; int channels = mChannel == AudioFormat.CHANNEL_IN_MONO ? 1 : 2; long byteRate = 16 * mSampleRate * channels / 8; byte[] data = new byte[mBufferSize]; try { in = new FileInputStream(inFilename); out = new FileOutputStream(outFilename); totalAudioLen = in.getChannel().size(); totalDataLen = totalAudioLen + 36; writeWaveFileHeader(out, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate); while (in.read(data) != -1) { out.write(data); out.flush(); } Log.e(TAG, "pcmToWav: 停止處理"); in.close(); out.close(); } catch (IOException e) { e.printStackTrace(); } } /** * 加入wav文件頭 */ private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen, long totalDataLen, long longSampleRate, int channels, long byteRate) throws IOException { byte[] header = new byte[44]; // RIFF/WAVE header header[0] = 'R'; 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); //WAVE header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E'; // 'fmt ' chunk header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' '; // 4 bytes: size of 'fmt ' chunk header[16] = 16; header[17] = 0; header[18] = 0; header[19] = 0; // format = 1 header[20] = 1; header[21] = 0; header[22] = (byte) channels; header[23] = 0; header[24] = (byte) (longSampleRate & 0xff); header[25] = (byte) ((longSampleRate >> 8) & 0xff); header[26] = (byte) ((longSampleRate >> 16) & 0xff); header[27] = (byte) ((longSampleRate >> 24) & 0xff); header[28] = (byte) (byteRate & 0xff); header[29] = (byte) ((byteRate >> 8) & 0xff); header[30] = (byte) ((byteRate >> 16) & 0xff); header[31] = (byte) ((byteRate >> 24) & 0xff); // block align header[32] = (byte) (2 * 16 / 8); header[33] = 0; // bits per sample header[34] = 16; header[35] = 0; //data header[36] = 'd'; 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); } }
釋放AudioRecord,錄制流程完畢
調用release()方法釋放資源
mAudioRecord.release();
最后你就可以在指定目錄下找到音頻文件播放了
最后介紹下其他API
獲取AudioRecord初始化狀態
public int getState() { return mState; }
注意!這里是初始化狀態,不是錄制狀態,它只會返回2個狀態
- AudioRecord#STATE_INITIALIZED //已經初始化
- AudioRecord#STATE_UNINITIALIZED //沒有初始化
獲取AudioRecord錄制狀態
public int getRecordingState() { synchronized (mRecordingStateLock) { return mRecordingState; } }
返回錄制狀態,它只返回2個狀態
- AudioRecord#RECORDSTATE_STOPPED //停止錄制
- AudioRecord#RECORDSTATE_RECORDING //正在錄制