AudioManager: android插上耳機仍然使用揚聲器播放音頻


手機音頻的輸出有外放(Speaker)、聽筒(Telephone Receiver)、有線耳機(WiredHeadset)、藍牙音箱(Bluetooth A2DP)等輸出設備。在平時,電話免提、插拔耳機、連接斷開藍牙設備等操作系統都會自動切換Audio音頻到相應的輸出設備上。比如電話免提就是從聽筒切換到外放揚聲器,插入耳機就是從外放切換到耳機。

場景需求

Android系統自動切換的這些策略,並不能全部滿足我們的產品需求,比如音樂App需要對聽歌時拔出耳機的操作進行阻止(暫停播放),防止突然切換到外放導致尷尬。

最近項目需求希望即使在連接藍牙音箱的情況下,仍舊使用手機外放播放音頻。這就需要強制切換Audio輸出通道,打破系統原有的策略。

查閱資料,看到了Android中可以通過AudioManager查詢、切換當前Audio輸出通道,並且在Audio輸出發生變化時,捕獲並處理這種變化。

首先提醒下大家,使用下面的方法時,需要添加權限:

<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

Audio輸出狀態查詢

AudioManager 提供的下列方法可以用來查詢當前Audio輸出的狀態:

  • isBluetoothA2dpOn():檢查A2DPAudio音頻輸出是否通過藍牙耳機;

  • isSpeakerphoneOn():檢查揚聲器是否打開;

  • isWiredHeadsetOn():檢查線控耳機是否連着;注意這個方法只是用來判斷耳機是否是插入狀態,並不能用它的結果來判定當前的Audio是通過耳機輸出的,這還依賴於其他條件。

  • setSpeakerphoneOn(boolean on):直接選擇外放揚聲器發聲;

  • setBluetoothScoOn(boolean on):要求使用藍牙SCO耳機進行通訊;

此處根據這篇文章簡單地介紹一下藍牙耳機的兩種鏈路,A2DP及SCO。android的api表明:

  • A2DP:是一種單向的高品質音頻數據傳輸鏈路,通常用於播放立體聲音樂;
  • SCO: 則是一種雙向的音頻數據的傳輸鏈路,該鏈路只支持8K及16K單聲道的音頻數據,只能用於普通語音的傳輸,若用於播放音樂那就只能呵呵了。

兩者的主要區別是:A2DP只能播放,默認是打開的,而SCO既能錄音也能播放,默認是關閉的。 如果要錄音肯定要打開sco啦,因此調用上面的 setBluetoothScoOn(boolean on) 就可以通過藍牙耳機錄音、播放音頻了,錄完、播放完記得要關閉。

另外,在Android系統中通過AudioManager.setMode()方法來管理播放模式。在setMode()方法中有以下幾種對應不同的播放模式:

  • MODE_NORMAL : 普通模式,既不是鈴聲模式也不是通話模式
  • MODE_RINGTONE : 鈴聲模式
  • MODE_IN_CALL : 通話模式
  • MODE_IN_COMMUNICATION : 通信模式,包括音/視頻,VoIP通話.(3.0加入的,與通話模式類似)

在設置播放模式的時候,需要考慮流類型,我在這里使用的流類型是 STREAM_MUSIC ,所以切換播放設備的時候就需要設置為MODE_IN_COMMUNICATION 模式而不是 MODE_NORMAL 模式。可以參考這個問題

解決問題

使用以下方法切換音頻Audio輸出,參考Android : Switching audio between Bluetooth and Phone Speaker is inconsistent

AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
/**
* 切換到外放
*/
public void changeToSpeaker(){
  //注意此處,藍牙未斷開時使用MODE_IN_COMMUNICATION而不是MODE_NORMAL
  mAudioManager.setMode(bluetoothIsConnected ? AudioManager.MODE_IN_COMMUNICATION : AudioManager.MODE_NORMAL);
  mAudioManager.stopBluetoothSco();
  mAudioManager.setBluetoothScoOn(false);
  mAudioManager.setSpeakerphoneOn(true);
}
/**
* 切換到藍牙音箱
*/
public void changeToHeadset(){
  mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
  mAudioManager.startBluetoothSco();
  mAudioManager.setBluetoothScoOn(true);
  mAudioManager.setSpeakerphoneOn(false);
}
/************************************************************/
//注意:以下兩個方法還未驗證
/************************************************************/
/**
* 切換到耳機模式
*/
public void changeToHeadset(){
  mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
  mAudioManager.stopBluetoothSco();
  mAudioManager.setBluetoothScoOn(false);
  mAudioManager.setSpeakerphoneOn(false);
}
/**
* 切換到聽筒
*/
public void changeToReceiver(){
  audioManager.setSpeakerphoneOn(false);
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
    audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
  } else {
    audioManager.setMode(AudioManager.MODE_IN_CALL);
  }
}

直接切換輸出通道的方法我們已經知道了。剩下需要解決的問題是,當藍牙設備斷開、連接的時候,我們希望可以自動切換到用戶原本設置的輸出通道上,比如在藍牙未連接時,用戶設置的是希望通過藍牙播報,所以應該在藍牙一旦連接以后,就把音頻切換到藍牙設備上。

下面我們就看看如何監聽藍牙設備的連接狀態。

首先注意使用前需要以下權限:

<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH" />

根據這篇文章,我們發現可以使用 AudioManager.ACTION_AUDIO_BECOMING_NOISY 這個Intent Action來監聽藍牙斷開、耳機插拔的廣播,但是測試發現,它也只能收到藍牙斷開的廣播,無法接收到藍牙連接的廣播,所以不是我們想要的。

進一步找到這篇文章:關於藍牙開發,必須注意的廣播,總結了以下藍牙廣播。

/**
* 有注釋的廣播,藍牙連接時都會用到
*/
intentFilter.addAction(BluetoothDevice.ACTION_FOUND); //搜索藍壓設備,每搜到一個設備發送一條廣播
intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); //配對開始時,配對成功時
intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); //配對時,發起連接
intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED);
intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); //配對結束時,斷開連接
intentFilter.addAction(PAIRING_REQUEST); //配對請求(Android.bluetooth.device.action.PAIRING_REQUEST)
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); //開始搜索
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); //搜索結束。重新搜索時,會先終止搜索
intentFilter.addAction(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); //本機開啟、關閉藍牙開關
intentFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED); //藍牙設備連接或斷開
intentFilter.addAction(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED); //更改藍牙名稱,打開藍牙時,可能會調用多次
intentFilter.addAction(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
intentFilter.addAction(BluetoothAdapter.ACTION_REQUEST_ENABLE);
intentFilter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); //搜索模式改變

我們發現了BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED 和 BluetoothAdapter.ACTION_STATE_CHANGED 這兩個Intent廣播。

那么這兩個廣播Intent的區別是什么呢?只用其中一個可以嗎?查看Google文檔發現

  • BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED :指的是本地藍牙適配器的連接狀態的發生改變(比如沒有關閉本機藍牙開關時,另外一個配對設備自己把連接斷開)

  • BluetoothAdapter.ACTION_STATE_CHANGED :指的是本地藍牙適配器的狀態已更改。 例如,藍牙開關打開或關閉。

換句話說,一個是用於連接狀態的變化,另一個用於藍牙適配器本身的狀態變化。經過測試發現,如果只使用BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED 監聽廣播,則會接收不到“主動關閉本機藍牙開關”的廣播事件。但只是用BluetoothAdapter.ACTION_STATE_CHANGED 的話,很明顯這時候藍牙設備並未真正配對。

動態注冊藍牙連接、斷開廣播的方式如下:

  • 動態注冊廣播
public class BluetoothConnectionReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent){
    if (BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { //藍牙連接狀態
      int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, -1);
      if (state == BluetoothAdapter.STATE_CONNECTED || state == BluetoothAdapter.STATE_DISCONNECTED) {
        //連接或失聯,切換音頻輸出(到藍牙、或者強制仍然揚聲器外放)
      }
    } else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())){ //本地藍牙打開或關閉
      int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
      if (state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_TURNING_OFF) {
      //斷開,切換音頻輸出
      }
    }
  }
}
BluetoothConnectionReceiver audioNoisyReceiver = new BluetoothConnectionReceiver();
//藍牙狀態廣播監聽
IntentFilter audioFilter = new IntentFilter();
audioFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
audioFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
mContext.registerReceiver(audioNoisyReceiver, audioFilter);

之后,我們就可以根據上面切換音頻輸出通道的代碼來實現藍牙設備連接、斷開以后強制打破操作系統原有的輸出通道切換策略,來實現我們自己想要的切換功能了。

本文轉自:Audio音頻輸出通道切換 - 藍牙、外放

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM