Android錄音功能的實現及踩坑記錄

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第5天,點選檢視活動詳情

最近接到個需求,不使用第三方SDK的情況下實現IM通訊,文字聊天已經通過MQTT實現,而語音功能目前想到的較好解決方案就是進行錄音檔案的上傳下載。可能還有更好解決方案,但我目前沒想到,有建議的小夥伴勞煩指導下。

前提

1、許可權申請: 清單檔案中加上: <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> 對應讀寫檔案和錄音許可權。

2、錄音檔案要寫到相應資料夾中,此資料夾要先建立,丟到Application或Activity的onCreate()中都可以,但一定要先建立。

程式碼實現流程

1、建立MediaRecorder物件;

2、呼叫setAudioSource()方法設定聲音的來源,一般傳入MediaRecorder.MIC;

3、呼叫setOutputFormat()設定所錄製的音訊檔案的格式;

4、呼叫setAudioRncoder()、setAudioEncodingBitRate(int bitRate)、setAudioSamlingRate(int SamplingRate)設定所錄音的編碼格式、編碼位率、取樣率等,當然不是每個都需要,根據具體業務調整(setAudioEncodingBitRate(96000),編碼位率一般是96000);

5、呼叫setOutputFile(String path)方法設定錄製的音訊檔案的儲存位置;

6、呼叫MediaRecoder物件的Prepare()方法準備錄製;

7、呼叫MediaRecoder物件的start()方法開始錄製;

8、結束後呼叫MediaRecoder物件的stop()方法停止錄製,並呼叫release()方法釋放資源。 示例如下: ``` public class TestActivity extends BaseActivity { private ActivityChatBinding testBinding; private MediaRecorder mediaRecorder; private boolean isRecorded;

@Override
public void initView() {
    testBinding = ActivityTestBinding.inflate(getLayoutInflater());
    setContentView(testBinding.getRoot());
    initMsgAndSth();
    checkPermission();
}

private void initMsgAndSth(){
    String record_Home = this.getFilesDir()+"/Sample";  //宣告儲存路徑,用絕對路徑什麼都可以
    //btnTalk就是個Button
    testBinding.btnTalk.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (isRecorded)
                stopRecordAudio();
            else
                startRecordAudio(record_Home);
        }
    });
}

private void stopRecordAudio() {
    //有的5.0機型上MediaRecorder.stop會報錯,這裡建議抓取一下異常
    if(mediaRecorder !=null){
        try {
            mediaRecorder.stop();//停止錄音
            mediaRecorder.release();//釋放資源
            mediaRecorder =null;
        }catch (Exception exception){
            mediaRecorder.reset();//重置
            mediaRecorder.release();//釋放資源
            mediaRecorder =null;
        }
        Toast.makeText(this,"停止錄音",Toast.LENGTH_SHORT).show();
    }
}

private void startRecordAudio(String path) {
    //資料夾一定要先建立,不然報錯的bug資訊中是找不到這裡的
    File audioFile = new File(path);
    if (!audioFile.exists()) {
        audioFile.mkdirs();
    } else if (!audioFile.isDirectory()) {
        audioFile.delete();
        audioFile.mkdirs();
    }
    File file = new File(path + "Sample.amr");
    if(!file.exists()){
        try {
            file.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    if(mediaRecorder == null){
        mediaRecorder = new MediaRecorder();
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);//設定麥克風
        /*
         * 設定儲存輸出檔案的格式:THREE_GPP/MPEG-4/RAW_AMR/Default THREE_GPP(3gp格式
         * ,H263影片/ARM音訊編碼)、MPEG-4、RAW_AMR(只支援音訊且音訊編碼要求為AMR_NB)
         */
        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB);

        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);//設定音訊檔案編碼格式
        mediaRecorder.setOutputFile(path+"Sample.amr");
    }
    try {
        mediaRecorder.prepare();  //start之前要先prepare
        mediaRecorder.start();
        isRecorded = true;
        Toast.makeText(this,"開始錄音",Toast.LENGTH_SHORT).show();
    } catch (IllegalStateException el){
        el.printStackTrace();
    } catch (RuntimeException e){
        e.printStackTrace();
    } catch (Exception e){
        e.printStackTrace();
    }
}

/**
 * 簡單的許可權申請邏輯
 */
private void checkPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        String[] permissions = new String[]{Manifest.permission.RECORD_AUDIO,Manifest.permission.WRITE_EXTERNAL_STORAGE};
        for (String permission : permissions) {
            if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this, permissions, 200);
                return;
            }
        }
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[]
        grantResults) {
    super.onRequestPermissionsResult(requestCode,permissions,grantResults);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && requestCode == 200) {
        for (int i = 0; i < permissions.length; i++) {
            if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                Intent intent = new Intent();
                intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                Uri uri = Uri.fromParts("package", getPackageName(), null);
                intent.setData(uri);
                startActivityForResult(intent, 200);
                return;
            }
        }
    }
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode == RESULT_OK && requestCode == 200) {
        checkPermission();
    }
}

} ``` 相關API差異已經寫的很詳細了,佈局很簡單,這裡就不貼出來了。這裡要特別注意的是呼叫順序不能改變,否則容易報錯,且因為呼叫順序不對而報錯的提示資訊也不一定足夠去定位問題。

踩坑

按照前面的提示,覺得避開所有坑能愉快的玩耍了,結果執行報錯,有的機型還不列印特定日誌,只能自己去鼓搗。

1、Android Q:

有的時候根據報錯分類,還是能抓到點蛛絲馬跡。如果是Android Q的裝置,報IO異常或者Permission Denied錯誤,則要檢查下清單檔案中application標籤裡有沒有這句: android:requestLegacyExternalStorage="true" 沒有的話一定要加上。

原因在於安卓10開始,要想訪問外部儲存的所有檔案,除了動態申請許可權許可權申明外,必須在主工程AndroidManifest.xml中加上這句,用於申請外部儲存所有檔案的許可權。

2、RuntimeException:setAudioSource failed

如果程式執行看到 RuntimeException: setAudioSource failed 報錯,請確保申請許可權相關邏輯正確,還有清單檔案中相關許可權的申請,但如果(雖然是極少概率,但我碰到了)新增許可權後,依舊還報這個錯,請進入手機設定-應用,找到你釋出上去的應用,給其授權。部分機型在除錯過程中除了第一次會提示授權外,再次安裝則不會再提示,這就相當於使用者沒有授予相關的錄音和sdcard讀寫許可權,程式依然會報錯。所以,建議每次開始進行錄音等邏輯前,進行一次邏輯判斷。

此外還有一種情況會出現此報錯,在錄音結束後沒有呼叫mediaRecorder.release()去釋放資源,而又處於stop狀態,這時候再去prepare、start容易報此錯,此時報錯列印堆疊與原先堆疊報錯資訊差別不大,較難定位,因此要格外注意。

3、MediaRecorder: stop failed

在呼叫start()後,馬上進行呼叫stop()的操作,由於沒有生成有效的音訊或是影片資料,會報此錯誤。這個情景在即時通訊過程中很常見,可以通過讓其執行緒睡眠小段時間(建議最少1秒),再stop()。官方文件註釋對此也有解釋:Note that a RuntimeException is intentionally thrown to the application, if no valid audio/video data has been received when stop() is called.