1. 程式人生 > >【Android】Audio音訊輸出通道切換

【Android】Audio音訊輸出通道切換

手機音訊的輸出有外放(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 模式。可以參考這個問題

解決問題

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);
}

/**
 * 切換到聽筒 http://blog.csdn.net/fxlysm/article/details/53302520
 */
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); //搜尋模式改變

那麼這兩個廣播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);

之後,我們就可以根據上面切換音訊輸出通道的程式碼來實現藍芽裝置連線、斷開以後強制打破作業系統原有的輸出通道切換策略,來實現我們自己想要的切換功能了。

參考資料: