Android Audio 音頻輸出通道切換

簡介: 手機音頻的輸出分有外放(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):

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

  2. isSpeakerphoneOn():檢查揚聲器是否打開铲球;

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

    note :這個方法只是用來判斷耳機是否是插入狀態(tài)晰赞。

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

  5. 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的源碼即可吃型。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末证鸥,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子勤晚,更是在濱河造成了極大的恐慌枉层,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赐写,死亡現(xiàn)場離奇詭異鸟蜡,居然都是意外死亡,警方通過查閱死者的電腦和手機挺邀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門揉忘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人端铛,你說我怎么就攤上這事泣矛。” “怎么了禾蚕?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵您朽,是天一觀的道長。 經(jīng)常有香客問我换淆,道長哗总,這世上最難降的妖魔是什么几颜? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮魂奥,結果婚禮上菠剩,老公的妹妹穿的比我還像新娘。我一直安慰自己耻煤,他們只是感情好具壮,可當我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著哈蝇,像睡著了一般棺妓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上炮赦,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天怜跑,我揣著相機與錄音,去河邊找鬼吠勘。 笑死性芬,一個胖子當著我的面吹牛,可吹牛的內容都是我干的剧防。 我是一名探鬼主播植锉,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼峭拘!你這毒婦竟也來了俊庇?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤鸡挠,失蹤者是張志新(化名)和其女友劉穎辉饱,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拣展,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡彭沼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了备埃。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片溜腐。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖瓜喇,靈堂內的尸體忽然破棺而出挺益,到底是詐尸還是另有隱情,我是刑警寧澤乘寒,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布望众,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏烂翰。R本人自食惡果不足惜夯缺,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望甘耿。 院中可真熱鬧踊兜,春花似錦、人聲如沸佳恬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽毁葱。三九已至垫言,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間倾剿,已是汗流浹背筷频。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留前痘,地道東北人凛捏。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像芹缔,于是被迫代替她去往敵國和親坯癣。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內容