簡介: 手機音頻的輸出分有外放(Speaker)傅是、聽筒(telephone Receiver)摄欲、有線耳機(WiredHeadset)挣输、藍牙耳機(Bluetooth A2DP)等輸出齐饮。電話免提捐寥、插拔耳機、連接斷開藍牙設備等操作系統(tǒng)都會自動切換Audio音頻到相應的輸出設備上祖驱。例如握恳,電話免提就是從聽筒切換到外放揚聲器,插入耳機就是從外放切換到耳機捺僻。
APP 場景需求:
比如音樂app正在播放音樂乡洼,這時我們打開自有攜帶對講功能的app,需要關閉音樂播放匕坯。打開app中的
音頻播放束昵,此時要選擇音頻的模式為通話模式的音頻通道(根據(jù)插入的設備情況):
- 沒有外設:選擇外放的通話模式
- 先接入有線耳機,再接入藍牙耳機或者先接入藍牙耳機醒颖、再接入有線耳機:以最后一次接入設備的狀態(tài)為準妻怎。選擇對應通話模式的音頻通道
- 音頻播放途中,有線耳機斷開:需要監(jiān)聽Audio外設監(jiān)聽狀態(tài)泞歉,來做對應的處理
- 音頻播放途中逼侦,藍牙耳機斷開:需要監(jiān)聽Audio外設監(jiān)聽狀態(tài)匿辩,來做對應的處理
- 退出音頻播放:需要關閉音頻焦點,并釋放音頻通道
Audio輸出狀態(tài)查詢
note: <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> 首先需要添加權限
AudioManager 提供的下列方法可以用來查詢當前Audio輸出的狀態(tài):
isBluetoothA2dpOn()
:檢查A2DPAudio音頻輸出是否通過藍牙耳機榛丢;isSpeakerphoneOn()
:檢查揚聲器是否打開铲球;-
isWiredHeadsetOn()
:檢查線控耳機是否連著;note :這個方法只是用來判斷耳機是否是插入狀態(tài)晰赞。
setSpeakerphoneOn(boolean on)
:直接選擇外放揚聲器發(fā)聲-
setBluetoothScoOn(boolean on)
:要求使用藍牙SCO耳機進行通訊稼病;這里簡單介紹一下藍牙耳機的兩種鏈路:A2DP,SCO掖鱼。A2DP:是一種單向的高品質音頻數(shù)據(jù)傳輸鏈路然走,通常用于播放立體聲音樂;SCO: 則是一種雙向的音頻數(shù)據(jù)的傳輸鏈路戏挡,該鏈路只支持8K及16K單聲道的音頻數(shù)據(jù)芍瑞,只能用于普通語音的傳輸。兩者的主要區(qū)別是:A2DP只能播放褐墅,默認是打開的拆檬,而SCO既能錄音也能播放,默認是關閉的妥凳。 如果要錄音肯定要打開sco啦竟贯,因此調用上面的
setBluetoothScoOn(boolean on)
就可以通過藍牙耳機錄音、播放音頻了逝钥,錄完屑那、播放完記得要關閉。
Audio 播放模式
Android系統(tǒng)通過AudioManager.setModel()來管理音頻模式艘款。如下幾種模式:
-
MODE_NORMAL
: 普通模式齐莲,既不是鈴聲模式也不是通話模式 -
MODE_RINGTONE
: 鈴聲模式 -
MODE_IN_CALL
: 通話模式 -
MODE_IN_COMMUNICATION
: 通信模式,包括音/視頻,VoIP通話.(3.0加入的磷箕,與通話模式類似),因為app場景需要用到對講功能选酗,所以選擇音頻模式為通信模式MODE_IN_COMMUNICATION
)
在設置播放模式的時候,需要考慮流類型 STREAM_MUSIC
岳枷,所以切換播放設備的時候就需要設置為MODE_IN_COMMUNICATION
模式而不是 MODE_NORMAL
模式芒填。
好了,了解了以上音頻通道相關的知識后空繁。我們來對需求做對應的解決殿衰。
使用如下方法來解決切換音頻輸出:
public class AudioUtils {
private static int lastModel = -10;
/**
* 音頻外放
*/
public static void changeToSpeaker(Context context) {
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
//注意此處,藍牙未斷開時使用MODE_IN_COMMUNICATION而不是MODE_NORMAL
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);
audioManager.setSpeakerphoneOn(true);
}
/**
* 切換到藍牙音箱
*/
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);
}
}
接下來是當有線耳機/藍牙設備斷開盛泡、連接的時候闷祥,我們希望可以自動切換到用戶原本設置的輸出通道上,比如在藍牙未連接時傲诵,用戶設置的是手機外放凯砍;藍牙一旦連接以后箱硕,就把音頻切換到藍牙設備上。
下面我們就看看如何監(jiān)聽藍牙設備的連接狀態(tài)悟衩。
首先需要使用Android的權限如下:
<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來監(jiān)聽有線耳機剧罩、藍牙耳機連接狀態(tài)。
-
BluetoothAdapter.ACTION_STATE_CHANGED
:指的是本地藍牙適配器的狀態(tài)已更改座泳。 例如惠昔,藍牙開關打開或關閉。 - AudioManager.ACTION_AUDIO_BECOMING_NOISY這個Intent Action來監(jiān)聽藍牙斷開挑势、耳機插拔的廣播
動態(tài)注冊監(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) { // 耳機拔出
AudioUtils.changeToSpeaker(MicroContext.getApplication());
} else if (state == 1) { // 耳機插入
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ù)上面切換音頻輸出通道的代碼來實現(xiàn)音頻外設連接镇防、斷開以后強制打破操作系統(tǒng)原有的輸出通道切換策略,來實現(xiàn)我們自己想要的切換功能了潮饱。
這里有一點需要注意的是:當如果有其他音樂軟件在播放音樂時营罢,打開自有的app,需要關閉其他音樂饼齿。
代碼如下:
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);
當使用藍牙通道進行音頻通訊的時候,關閉音頻時就需要把音頻關閉掉蝙搔,然后把音頻模式切回原來的模式缕溉。
AudioUtils.dispose(MicroContext.getApplication(),afChangeListener);
dispose里面的代碼,請看AudioUtils的源碼即可吃型。