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方法寫到快取中,即可實現功能,具體實現參考程式碼。