Android Audio 音頻輸出通道切換

簡(jiǎn)介: 手機(jī)音頻的輸出分有外放(Speaker)、聽筒(telephone Receiver)、有線耳機(jī)(WiredHeadset)、藍(lán)牙耳機(jī)(Bluetooth A2DP)等輸出。電話免提、插拔耳機(jī)、連接斷開藍(lán)牙設(shè)備等操作系統(tǒng)都會(huì)自動(dòng)切換Audio音頻到相應(yīng)的輸出設(shè)備上。例如,電話免提就是從聽筒切換到外放揚(yáng)聲器,插入耳機(jī)就是從外放切換到耳機(jī)。

APP 場(chǎng)景需求:

比如音樂app正在播放音樂,這時(shí)我們打開自有攜帶對(duì)講功能的app,需要關(guān)閉音樂播放。打開app中的

音頻播放,此時(shí)要選擇音頻的模式為通話模式的音頻通道(根據(jù)插入的設(shè)備情況):

- 沒有外設(shè):選擇外放的通話模式
- 先接入有線耳機(jī),再接入藍(lán)牙耳機(jī)或者先接入藍(lán)牙耳機(jī)、再接入有線耳機(jī):以最后一次接入設(shè)備的狀態(tài)為準(zhǔn)。選擇對(duì)應(yīng)通話模式的音頻通道
- 音頻播放途中,有線耳機(jī)斷開:需要監(jiān)聽Audio外設(shè)監(jiān)聽狀態(tài),來(lái)做對(duì)應(yīng)的處理
- 音頻播放途中,藍(lán)牙耳機(jī)斷開:需要監(jiān)聽Audio外設(shè)監(jiān)聽狀態(tài),來(lái)做對(duì)應(yīng)的處理
- 退出音頻播放:需要關(guān)閉音頻焦點(diǎn),并釋放音頻通道

Audio輸出狀態(tài)查詢

note: <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> 首先需要添加權(quán)限

AudioManager 提供的下列方法可以用來(lái)查詢當(dāng)前Audio輸出的狀態(tài):

  1. isBluetoothA2dpOn():檢查A2DPAudio音頻輸出是否通過(guò)藍(lán)牙耳機(jī);

  2. isSpeakerphoneOn():檢查揚(yáng)聲器是否打開;

  3. isWiredHeadsetOn():檢查線控耳機(jī)是否連著;

    note :這個(gè)方法只是用來(lái)判斷耳機(jī)是否是插入狀態(tài)。

  4. setSpeakerphoneOn(boolean on):直接選擇外放揚(yáng)聲器發(fā)聲

  5. setBluetoothScoOn(boolean on):要求使用藍(lán)牙SCO耳機(jī)進(jìn)行通訊;

    這里簡(jiǎn)單介紹一下藍(lán)牙耳機(jī)的兩種鏈路:A2DP,SCO。A2DP:是一種單向的高品質(zhì)音頻數(shù)據(jù)傳輸鏈路,通常用于播放立體聲音樂;SCO: 則是一種雙向的音頻數(shù)據(jù)的傳輸鏈路,該鏈路只支持8K及16K單聲道的音頻數(shù)據(jù),只能用于普通語(yǔ)音的傳輸。兩者的主要區(qū)別是:A2DP只能播放,默認(rèn)是打開的,而SCO既能錄音也能播放,默認(rèn)是關(guān)閉的。 如果要錄音肯定要打開sco啦,因此調(diào)用上面的setBluetoothScoOn(boolean on)就可以通過(guò)藍(lán)牙耳機(jī)錄音、播放音頻了,錄完、播放完記得要關(guān)閉。

Audio 播放模式

Android系統(tǒng)通過(guò)AudioManager.setModel()來(lái)管理音頻模式。如下幾種模式:

  • MODE_NORMAL : 普通模式,既不是鈴聲模式也不是通話模式
  • MODE_RINGTONE : 鈴聲模式
  • MODE_IN_CALL : 通話模式
  • MODE_IN_COMMUNICATION : 通信模式,包括音/視頻,VoIP通話.(3.0加入的,與通話模式類似),因?yàn)閍pp場(chǎng)景需要用到對(duì)講功能,所以選擇音頻模式為通信模式MODE_IN_COMMUNICATION)

在設(shè)置播放模式的時(shí)候,需要考慮流類型 STREAM_MUSIC ,所以切換播放設(shè)備的時(shí)候就需要設(shè)置為MODE_IN_COMMUNICATION 模式而不是 MODE_NORMAL 模式。

好了,了解了以上音頻通道相關(guān)的知識(shí)后。我們來(lái)對(duì)需求做對(duì)應(yīng)的解決。

使用如下方法來(lái)解決切換音頻輸出:


public class AudioUtils {
    private static int lastModel = -10;
    /**
     * 音頻外放
     */
    public static void changeToSpeaker(Context context) {
        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        //注意此處,藍(lán)牙未斷開時(shí)使用MODE_IN_COMMUNICATION而不是MODE_NORMAL
        audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
        audioManager.stopBluetoothSco();
        audioManager.setBluetoothScoOn(false);
        audioManager.setSpeakerphoneOn(true);
    }

    /**
     * 切換到藍(lán)牙音箱
     */
    public static void changeToHeadset(Context context) {
        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
        audioManager.startBluetoothSco();
        audioManager.setBluetoothScoOn(true);
        audioManager.setSpeakerphoneOn(false);
    }

    /**
     * 切換到聽筒
     */
    public static void changeToReceiver(Context context) {
        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        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);
        }
    }


    public static void dispose(Context context, AudioManager.OnAudioFocusChangeListener focusRequest) {
        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        audioManager.setMode(lastModel);
        if (audioManager.isBluetoothScoOn()) {
            audioManager.setBluetoothScoOn(false);
            audioManager.stopBluetoothSco();
        }
        audioManager.unloadSoundEffects();
        if (null != focusRequest) {
            audioManager.abandonAudioFocus(focusRequest);
        }
    }


    public static void getModel(Context context) {
        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        lastModel = audioManager.getMode();
    }

    public static void changeToNomal(Context context) {
        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        audioManager.setMode(AudioManager.MODE_NORMAL);
    }

    public static boolean isWiredHeadsetOn(Context context) {
        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        return audioManager.isWiredHeadsetOn();
    }

    public static boolean isBluetoothA2dpOn(Context context) {
        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        return audioManager.isBluetoothA2dpOn();
    }

    /**
     * context 傳入的是MicroContext.getApplication()
     * @param context
     */
    public static void choiceAudioModel(Context context) {
        if (isWiredHeadsetOn(context)) {
            changeToReceiver(context);
        } else if (isBluetoothA2dpOn(context)) {
            changeToHeadset(context);
        } else {
            changeToSpeaker(context);
        }
    }

    public static void pauseMusic(Context context, AudioManager.OnAudioFocusChangeListener focusRequest) {
        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        audioManager.requestAudioFocus(focusRequest, AudioManager.STREAM_MUSIC, AUDIOFOCUS_GAIN);
    }


}

接下來(lái)是當(dāng)有線耳機(jī)/藍(lán)牙設(shè)備斷開、連接的時(shí)候,我們希望可以自動(dòng)切換到用戶原本設(shè)置的輸出通道上,比如在藍(lán)牙未連接時(shí),用戶設(shè)置的是手機(jī)外放;藍(lán)牙一旦連接以后,就把音頻切換到藍(lán)牙設(shè)備上。

下面我們就看看如何監(jiān)聽藍(lán)牙設(shè)備的連接狀態(tài)。
首先需要使用Android的權(quán)限如下:

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

然后使用Intent.ACTION_HEADSET_PLUG,BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED,AudioManager.ACTION_AUDIO_BECOMING_NOISY來(lái)監(jiān)聽有線耳機(jī)、藍(lán)牙耳機(jī)連接狀態(tài)。

  • BluetoothAdapter.ACTION_STATE_CHANGED :指的是本地藍(lán)牙適配器的狀態(tài)已更改。 例如,藍(lán)牙開關(guān)打開或關(guān)閉。
  • AudioManager.ACTION_AUDIO_BECOMING_NOISY這個(gè)Intent Action來(lái)監(jiān)聽藍(lán)牙斷開、耳機(jī)插拔的廣播

動(dòng)態(tài)注冊(cè)監(jiān)聽廣播:

public class HeadsetPlugReceiver extends BroadcastReceiver {
    private static final String TAG = "HeadsetPlugReceiver";
    private AudioManager audioManager;

    @Override
    public void onReceive(Context context, Intent intent) {
        audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        String action = intent.getAction();
        if (action.equals(Intent.ACTION_HEADSET_PLUG)) {
            int state = intent.getIntExtra("state", 0);
            if (state == 0) {   // 耳機(jī)拔出
                AudioUtils.changeToSpeaker(MicroContext.getApplication());
            } else if (state == 1) {    // 耳機(jī)插入
                AudioUtils.changeToReceiver(MicroContext.getApplication());
            }
        } else if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
            int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
                    BluetoothHeadset.STATE_DISCONNECTED);
            updateBluetoothIndication(state);
        } else if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(action)) {
            AudioUtils.changeToSpeaker(MicroContext.getApplication());
        }
    }

    public void updateBluetoothIndication(int bluetoothHeadsetState) {
        if (bluetoothHeadsetState == BluetoothProfile.STATE_CONNECTED) {
            L.i(TAG, "BluetoothProfile.STATE_CONNECTED");
            if (audioManager == null) {
                return;
            } else {
                AudioUtils.changeToHeadset(MicroContext.getApplication());
            }
        } else {
            if (audioManager == null) {
                return;
            } else {
                AudioUtils.changeToSpeaker(MicroContext.getApplication());
            }
        }
    }
}
private HeadsetPlugReceiver mHeadsetPlugReceiver;
    @Override
    protected void onResume() {
        super.onResume();
        registerHeadSetPlugListener();
        AudioUtils.pauseMusic(MicroContext.getApplication(),afChangeListener);

        isFront = true;
        L.e("Current ClassName:", getClass().getSimpleName());
    }

 @Override
    protected void onPause() {
        super.onPause();
        isFront = false;
        if (null != mHeadsetPlugReceiver) {
            unregisterReceiver(mHeadsetPlugReceiver);
        }
        AudioUtils.dispose(MicroContext.getApplication(),afChangeListener);
    }

這樣我們就能根據(jù)上面切換音頻輸出通道的代碼來(lái)實(shí)現(xiàn)音頻外設(shè)連接、斷開以后強(qiáng)制打破操作系統(tǒng)原有的輸出通道切換策略,來(lái)實(shí)現(xiàn)我們自己想要的切換功能了。

這里有一點(diǎn)需要注意的是:當(dāng)如果有其他音樂軟件在播放音樂時(shí),打開自有的app,需要關(guān)閉其他音樂。

代碼如下:

    private AudioManager.OnAudioFocusChangeListener afChangeListener = new AudioManager.OnAudioFocusChangeListener() {
        @Override
        public void onAudioFocusChange(int focusChange) {
            if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
                // Pause playback
            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
                // Stop playback
            } else if (focusChange == AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
                // Lower the volume
            } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
                // Resume playback or Raise it back to normal
            }
        }
    };

AudioUtils.pauseMusic(MicroContext.getApplication(),afChangeListener);

當(dāng)使用藍(lán)牙通道進(jìn)行音頻通訊的時(shí)候,關(guān)閉音頻時(shí)就需要把音頻關(guān)閉掉,然后把音頻模式切回原來(lái)的模式。

AudioUtils.dispose(MicroContext.getApplication(),afChangeListener);

dispose里面的代碼,請(qǐng)看AudioUtils的源碼即可。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容