参考:

【Android】Audio音频输出通道切换 - 蓝牙、外放

Android Audio 音频输出通道切换

为什么 iOS 或 Android 设备连接蓝牙设备后不能通过蓝牙设备接电话?

xq_android / MyAudioTrackDemo1

手机音频的输出有外放(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:


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

直接切换输出通道的方法我们已经知道了。

剩下需要解决的问题是,当蓝牙设备断开、连接的时候,我们希望可以自动切换到用户原本设置的输出通道上,
比如在蓝牙未连接时,用户设置的是希望通过蓝牙播报,所以应该在蓝牙一旦连接以后,就把音频切换到蓝牙设备上。

下面我们就看看如何监听蓝牙设备的连接状态。

监听蓝牙连接状态

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

<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_CHANGEDBluetoothAdapter.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 {@Overridepublic 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);

之后,我们就可以根据上面切换音频输出通道的代码来实现蓝牙设备连接、断开以后强制打破操作系统原有的输出通道切换策略,来实现我们自己想要的切换功能了。

为什么 iOS 或 Android 设备连接蓝牙设备后不能通过蓝牙设备接电话?

为什么 iOS 或 Android 设备连接蓝牙设备后不能通过蓝牙设备接电话?

问题原因

  • 未在 AndroidManifest.xml 文件中添加使用蓝牙设备的权限。
  • 只有支持 SCO 协议的蓝牙设备,才支持录制和播放音频。如果蓝牙设备只支持 A2DP 协议,则无法录制音频,那么用户就无法使用该蓝牙设备通话。
  • 只有 Android 设备支持在未进行系统通话时使用蓝牙 SCO,才能将语音路由切换为蓝牙设备。

解决方案

在 AndroidManifest.xml 文件中添加如下行,获取使用蓝牙设备的权限:

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

确认蓝牙设备是否支持 SCO 协议。如果不支持,切换支持 SCO 协议的蓝牙设备。

调用 Android 原生 API AudioManmager.isBluetoothScoAvailableOffCall 方法判断该 Android 设备是否支持在没有系统通话时使用蓝牙 SCO。
如果设备不支持该功能,则无法将音频路由切换为蓝牙设备,只能使用系统默认的路由接听电话。

【Android】Audio音频输出通道切换 - 蓝牙bluetooth、外放相关推荐

  1. 【Android】Audio音频输出通道切换 - 蓝牙、外放

    手机音频的输出有外放(Speaker).听筒(Telephone Receiver).有线耳机(WiredHeadset).蓝牙音箱(Bluetooth A2DP)等输出设备.在平时,电话免提.插拔耳 ...

  2. Android Audio 音频输出设备切换

    扬声器输出和耳机输出是音频系统最基本的音频输出通路,实际应用中用户可根据自己需要随意切换,具体表现是通过插入耳机选择耳机输出,拔出耳机选择默认的扬声器输出. 如图5-13所示,扬声器与耳机输出切换的实 ...

  3. Android Studio 基础 之 获取蓝牙Bluetooth 的状态,设置的蓝牙Bluetooth 的开关状态,并监听蓝牙Bluetooth 的状态变化方法整理

    Android Studio 基础 之 获取蓝牙Bluetooth 的状态,设置的蓝牙Bluetooth 的开关状态,并监听蓝牙Bluetooth 的状态变化方法整理 目录 Android Studi ...

  4. Android Audio音频通路(七)

    android audio 生产者与消费者 简介 全面接触生产者/消费者问题是在操作系统原理中,并发性原理讨论的问题 生产者/消费者问题.最近的工作偏向音频,接着上一篇文章,用生产者,消费者模型来理解 ...

  5. android系统源码学习 -- 蓝牙 bluetooth (一) 入门

    原文地址:http://blog.csdn.net/baimy1985/article/details/8892410 1. 代码分布: packages/apps/Bluetooth/ 看这路径肯定 ...

  6. <一>Android Audio音频框架

    目录 1.0 设备驱动 2.0  android hal层 3.0  选择设备的暗箱策略AudioPolicy 4.0  软件层面的混音,AudioFlinger 5.0 完整的对外接口 AudioS ...

  7. Android SoundPool插入耳机后依然有外放声音

    使用soundPool播放声音,当手机已经接通耳机时,还会有外放声音,是因为在初始化soundpool是用的流类型(streamType)导致的,有些流类型系统是一定会播放外放的,比如警告类型和响铃类 ...

  8. 关于公共场合音频(抖音)外放现象的调查

    一.第1次调研(以问卷形式) 由以上问卷,我们得知调查者或多或少有过被外放或外放的经历,不过鲜有人上前提醒,大多采取默默忍受的方式. 二.第2次调研(换位思考) 外放者 Seeing (换位思考) 1 ...

  9. Android Audio 音频路由

    一.在 AudioPolicyService 中,选择音频路由时会从当前音频流的类型获取音频的路由策略: /frameworks/av/services/audiopolicy/managerdefa ...

最新文章

  1. python部署工具fabric
  2. 【jq插件】Grade-打分效果
  3. 使用OUTPUT从句从SQL Server表删除和归档大量记录
  4. python三本经典书籍-有哪些 Python 经典书籍?
  5. 樊昌信 通信原理第七版 第八章思考题
  6. PDF文件编辑密码解除
  7. 同步时钟配置电脑自动校时
  8. 三维扫描用于3D打印技术
  9. ps怎么更改背景图层大小_如何利用Photoshop软件修改图片尺寸的大小
  10. 依图三变,“退群”AI四小龙
  11. 调用微信扫一扫接口/利用微信JS-SDK调用微信扫一扫功能
  12. 在Excel中如何提取括号中的数字
  13. java 将网页表格导出_Java导出网页表格Excel过程详解
  14. IDA PRO 静态反汇编与OllyDbg动态调试实战技巧汇总
  15. 大三,请问现在自学Java还来得及吗?
  16. 机器学习作业-交通流量预测综述
  17. Vulnhub_gigachad_vh
  18. 解决java web中safari浏览器下载后文件中文乱码问题
  19. 计算机组成原理实验运动码表,计算机组成原理
  20. 韩国社交软件Kakao Talk要开网络银行,社交软件+银行的模式会怎么转?

热门文章

  1. 初中数学抽象教学的案例_初中数学情景教学案例
  2. 军职在线大学生计算机基础,军职在线演讲与口才答案第一章
  3. 关于FTP服务器出现227 Entering Passive Mode的解决方法
  4. 数据在网络中的传输(一)
  5. 二代身份证的尺寸大小
  6. pheatmap画热图初试
  7. 【Apple】关于iCloud家庭共享的付款方式 / 如何改变iCloud家庭共享付款方式
  8. 如何描述实证结果的经济显著性?
  9. 【python爬虫学习篇】请求模块urllib3
  10. Fresnel效果和颜色色散(Using OpenGL Cg)