1. 程式人生 > >Android使用藍芽錄音和播放

Android使用藍芽錄音和播放

簡介

實現一個可以邊錄邊播的工具,將藍芽耳機麥克風錄到的聲音從耳機中播放出來。最近在做一個語音助手工具軟體,具體需求是使用藍芽耳機喚醒APP並講話,APP將講話內容進行語音識別,通過雲平臺進行理解並返回相應的操作。比如當用戶說“播放音樂”的時候,APP將會隨機播放一首歌。期間在藍芽耳機錄音和播放中遇到了很多問題,APP錄不到聲音,聲音從手機聽筒播放,沒有任何聲音等等等。因此實現了這個BTRecorder DEMO,記錄一些藍芽錄音及播放的問題,也方便後續做一些功能測試。

Android錄音(三種方式錄音)

1、通過Intent呼叫系統的錄音機進行錄音

通過傳送一個Intent,系統開啟錄音機進行錄音,錄音完成之後,在onActivityResult中返回錄音檔案的URI,此時我們便可以使用MediaPlayer進行錄音的播放。該方法使用簡單方便,只需要幾句程式碼便可完成錄音操作。然而由於使用的是系統錄音機進行錄音,我們沒辦法對其進行更多的操作,使用起來非常不方便,因此該方法一般不適用於APP的錄音需求。呼叫例項:

private final static int REQUEST_RECORDER = 1;
private Uri uri;
public void startRecorder(){

    Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
  
  startActivityForResult(intent,REQUEST_RECORDER);
}
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {     super.onActivityResult(requestCode, resultCode, data);
    if (resultCode == RESULT_OK && REQUEST_RECORDER == requestCode){
        uri = data.getData();
    }
}

2、使用MediaRecorder進行錄音

先來看一下使用例項:

MediaRecorder recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
recorder.setOutputFile(PATH_NAME);
recorder.prepare();
recorder.start(); // Recording is now started
// Recoding...
recorder.stop();
recorder.reset(); // You can reuse the object by going back to setAudioSource() step
recorder.release(); // Now the object cannot be reused

MediaRecorder可用來錄製音訊和視訊。在使用時,為了能夠捕獲音訊,在例項化MediaRecorder之後,需要呼叫setAudioSource和setAudioEncoder方法。如果沒有呼叫這兩個方法,音訊、視訊將不會被錄製,通常在使用時,還要呼叫setOutputFormat和setOutputFile兩個方法設定錄音檔案的資訊。

setAudioSource

設定錄音的音訊源,定義在MediaRecorder.AudioSource中。預設情況下可以使用MediaRecorder.AudioSource.DEFAULT或者MediaRecorder.AudioSource.MIC。如果想要使用藍芽耳機的麥克風進行錄音,則需要設定為MediaRecorder.AudioSource.VOICE_COMMUNICATION。如果沒有設定為VOICE_COMMUNICATION,可能在部分手機上無法實現藍芽耳機錄音。

setOutputFormat

設定輸出檔案的格式,該方法必須在setAudioSource()/setVideoSource()之後,prepare()之前呼叫。通常使用MediaRecorder.OutputFormat.THREE_GPP制定輸出3GP檔案,使用MediaRecorder.OutputFormat.MPEG_4制定輸出MP4檔案。

setAudioEncoder

設定用於錄製的編碼器,如果未呼叫此方法,則輸出檔案將不包含音軌。在setOutputFormat()之後但在prepare()之前呼叫。通常設定為MediaRecorder.AudioEncoder.AMR_NB。

3、使用AudioRecord錄製原始音訊

使用AudioRecord類進行音訊錄製是三種音訊錄製方法中最為靈活的,它能直接得到錄音的資料流,可以對資料流進行處理,從而實現更多有趣的功能。使用AudioRecord錄音也很簡單,我們只需要構造一個AudioRecord例項物件,並傳入不同的引數。

AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)

audioSource:音訊源,和MediaRecorder中的一致。

sampleRateInHz取樣率,44100Hz是目前唯一保證可在所有裝置上工作的速率。一般藍芽耳機無法達到44100Hz的取樣率,所有在使用藍芽耳機錄音的時候,設定為8000Hz或者16000Hz。

channelConfig:描述音訊通道的配置。一般可設定為AudioFormat.CHANNEL_IN_MONO,它可以保證在所有裝置上執行。

audioFormat:返回音訊資料的格式。常用的可以設定為ENCODING_PCM_8BIT、ENCODING_PCM_16BIT。表示我們使用8位或者16為的PCM資料作為返回。PCM代表脈衝編碼調製(Pulse Code Modulation),他實際上是原始的音訊樣本。因此能夠設定每一個樣本的解析度為16位或8位。16位將佔用很多其它的控制元件和處理能力,但表示的音訊將更接近真實。

bufferSizeInBytes:指定緩衝區的大小,使用時,一般我們通過AudioRecord來查詢最小的緩衝區大小。

下面來看一下建立AudioRecord例項的程式碼:

int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE_HZ, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT) * 2
AudioRecord audioRecord = AudioRecord(MediaRecorder.AudioSource.VOICE_COMMUNICATION, SAMPLE_RATE_HZ, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize)

建立完AudioRecord例項後,我們必須建立一個非同步的任務或者執行緒來獲取錄音資料。

internal inner class RecordThread : Thread() {
private val audioRecord: AudioRecord
private val bufferSize: Int
private var isRun: Boolean = false
init {
    var audiosource = MediaRecorder.AudioSource.VOICE_RECOGNITION
    this.bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE_HZ, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT) * 2
    this.audioRecord = AudioRecord(audiosource, SAMPLE_RATE_HZ, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, this.bufferSize)
}
override fun run() {
    super.run()
    this.isRun = true
    try {
        if (audioRecord.state == 1) {
            this.audioRecord.startRecording()
            mStartTime = System.currentTimeMillis()
            while (this.isRun) {
                val buffer = ByteArray(bufferSize)
                val readBytes = audioRecord.read(buffer, 0, bufferSize)
                if (readBytes > 0) {
                    val valume = calculateVolume(buffer)                        Log.e("MediaRecord", "Volume() --> " + valume)
                }
            }
            try {
               this.audioRecord.stop()
               this.audioRecord.release()
            }catch (audioException: Exception){
            }
        }
    } catch (e2: Exception) {
        try {
            this.audioRecord.stop()
            this.audioRecord.release()
        }catch (audioException: Exception){
        }
        isRun = false
    }
}
fun pause() {
    this.isRun = false
    try {
        this.audioRecord.stop()
        this.audioRecord.release()
    }catch (e: Exception){
    }
}
@Synchronized override fun start() {
    if (!isRun) {
        super.start()
    }
}
// 計算錄音音量
private fun calculateVolume(buffer: ByteArray): Int {
    val audioData = ShortArray(buffer.size / 2)
   ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(audioData)
    var sum = 0.0 // 將 buffer 內容取出,進行平方和運算
    for (i in audioData.indices) {
        sum += (audioData[i] * audioData[i]).toDouble()
    }
    // 平方和除以資料總長度,得到音量大小
    val mean = sum / audioData.size.toDouble(
    val volume = 10 * Math.log10(mean)
    return volume.toInt()
}
}

Android播放聲音(三種方式)

1、SoundPool播放音訊

SoundPool支援多個音訊檔案同時播放(組合音訊也是有上限的),延時短,比較適合短促、密集的場景,是遊戲開發中音效播放的福音。SoundPool只適合短促的音效播放,不能用於長時間的音樂播放。

1) 將音訊檔案複製到Raw目錄中

2)使用SoundPool.Builder()進行例項化

3)載入音訊檔案load(Context context, int resId, int priority)

4)設定載入完成回撥物件

5)在載入完成回撥中播放聲音play(int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate)

 6)在不需要的時候釋放資源release()

具體可參考下面的程式碼實現:

// 初始化方法,例項化SoundPool物件。
fun initSoundPool() {
    val attributes = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build()
    soundPool = SoundPool.Builder() .setAudioAttributes(attributes) .setMaxStreams(1) .build()
}
fun playNotif() {
    try {
        try {
            // 載入音訊檔案,音訊檔案存放於Raw目錄下。
            soundPool!!.load(BaseApplication.getContext(), R.raw.ding, 0)
            soundPool!!.setOnLoadCompleteListener { soundPool, sampleId, status ->
                soundPool.play(sampleId,0.7f, 0.7f, 0, 0, 1.0f)
            }
        } catch (e: Exception) {
            if (BaseApplication.DEBUG) {
                e.printStackTrace()
            }
            soundPool!!.release()
        }
    } catch (e: Exception) {
    }
}

程式碼中,需要注意的是在初始化方法中,.setUsage()的引數設定為AudioAttributes.USAGE_MEDIA表示聲音型別為多媒體型別,使用藍芽耳機的通話模式下是聽不到聲音的;使用AudioAttributes.USAGE_VOICE_COMMUNICATION則可以使藍芽耳機在通話模式下也能聽到聲音,其主要原因還是和藍芽耳機的通訊鏈路相關。

2、MediaPlayer

對於android音訊的播放,MediaPlayer確實強大而且方便使用,提供了對音訊播放的各種控制,支援AAC、AMR、FLAC、MP3、MIDI、OGG、PCM等格式 ,生命週期:


使用時,建立一個MediaPlayer例項,設定資料來源,不要忘記prepare(),儘量使用非同步prepareAync(),這樣不會阻塞UI執行緒,播放完畢即使釋放資源。

mediaPlayer.stop()
mediaPlayer.release()
mediaPlayer = null

A)直接播放Raw目錄中的音訊檔案

建立物件的時候直接指定檔案ID,不需要設定setDataSource;不需要prepare()。

val meidaplayer = MediaPlayer.create(mContext, R.raw.network3)
meidaplayer.start()
meidaplayer.setOnCompletionListener {    meidaplayer.release()}

B)播放SD卡或網路上的音訊檔案

val mPlayer = MediaPlayer()
mPlayer.setOnPreparedListener(MyOnPrepareListener())
mPlayer.setOnCompletionListener(MyOnCompletionListener())// 播放SD卡音訊
mPlayer.setDataSource("../music/test.mp3")
// 播放網路音訊
// mPlayer.setDataSource("https://../test.mp3")
mPlayer.prepareAsync();
mPlayer.start()

C)播放Asset目錄中的音訊檔案

val mPlayer = MediaPlayer()
mPlayer.setOnPreparedListener(MyOnPrepareListener())
mPlayer.setOnCompletionListener(MyOnCompletionListener())
val fd = getAssets().openFd("samsara.mp3");
mPlayer.setDataSource(fd)
mPlayer.prepareAsync();
mPlayer.start()

3、AudioTrack

AudioTrack是管理和播放單一音訊資源的類。它用於PCM音訊流的回放,實現方式是通過write方法把資料push到AudioTrack物件。簡單的應用可以參考下面的程式碼:

private var audioBufSize: Int = 0
private var player: AudioTrack? = null
// 初始化
audioBufSize = AudioTrack.getMinBufferSize(8000,            AudioFormat.CHANNEL_OUT_STEREO,            AudioFormat.ENCODING_PCM_16BIT)
player = AudioTrack(AudioManager.STREAM_VOICE_CALL, 8000,        AudioFormat.CHANNEL_OUT_STEREO,        AudioFormat.ENCODING_PCM_16BIT,        audioBufSize,        AudioTrack.MODE_STREAM)
// 呼叫播放方法啟動播放器
player!!.play()

上面的程式碼執行之後,播放器就開始播放了,只是現在沒有資料推送到AudioTrack,所以聽不到聲音。我們將麥克風採集到的PCM資料或解碼後的PCM資料通過wirte方法寫到AudioTarck快取中,此時就能聽到聲音了。

player!!.write(buffer, 0, readBytes)

需要停止播放的時候,只要呼叫stop()方法即可停止播放。

player!!.stop()

實時錄音播放

上面講到了Android的錄音和播放,我們使用AudioRecord,將獲取到的PCM資料直接通過AudioTrack的write方法寫到快取中,即可實現功能,具體實現參考程式碼。