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

2024-05-26 22:32

本文主要是介绍【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 模式。可以参考这个问题

解决问题

使用以下方法切换音频Audio输出,参考Android : Switching audio between Bluetooth and Phone Speaker is inconsistent:

AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);/*** 切换到外放*/
public void changeToSpeaker(){//注意此处,蓝牙未断开时使用MODE_IN_COMMUNICATION而不是MODE_NORMALmAudioManager.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); //搜索模式改变

我们发现了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);

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

参考资料:

1、Android中的Audio播放:控制Audio输出通道切换
2、Android音乐播放模式切换-外放、听筒、耳机
3、Android : Switching audio between Bluetooth and Phone Speaker is inconsistent
4、Listening to bluetooth connections
5、 Android 5.0及以上平台 听筒/喇叭播放 AudioManager.MODE_IN_COMMUNICATION

这篇关于【Android】Audio音频输出通道切换 - 蓝牙、外放的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1005773

相关文章

2.1/5.1和7.1声道系统有什么区别? 音频声道的专业知识科普

《2.1/5.1和7.1声道系统有什么区别?音频声道的专业知识科普》当设置环绕声系统时,会遇到2.1、5.1、7.1、7.1.2、9.1等数字,当一遍又一遍地看到它们时,可能想知道它们是什... 想要把智能电视自带的音响升级成专业级的家庭影院系统吗?那么你将面临一个重要的选择——使用 2.1、5.1 还是

IDEA如何切换数据库版本mysql5或mysql8

《IDEA如何切换数据库版本mysql5或mysql8》本文介绍了如何将IntelliJIDEA从MySQL5切换到MySQL8的详细步骤,包括下载MySQL8、安装、配置、停止旧服务、启动新服务以及... 目录问题描述解决方案第一步第二步第三步第四步第五步总结问题描述最近想开发一个新应用,想使用mysq

C++中实现调试日志输出

《C++中实现调试日志输出》在C++编程中,调试日志对于定位问题和优化代码至关重要,本文将介绍几种常用的调试日志输出方法,并教你如何在日志中添加时间戳,希望对大家有所帮助... 目录1. 使用 #ifdef _DEBUG 宏2. 加入时间戳:精确到毫秒3.Windows 和 MFC 中的调试日志方法MFC

Python使用Colorama库美化终端输出的操作示例

《Python使用Colorama库美化终端输出的操作示例》在开发命令行工具或调试程序时,我们可能会希望通过颜色来区分重要信息,比如警告、错误、提示等,而Colorama是一个简单易用的Python库... 目录python Colorama 库详解:终端输出美化的神器1. Colorama 是什么?2.

React实现原生APP切换效果

《React实现原生APP切换效果》最近需要使用Hybrid的方式开发一个APP,交互和原生APP相似并且需要IM通信,本文给大家介绍了使用React实现原生APP切换效果,文中通过代码示例讲解的非常... 目录背景需求概览技术栈实现步骤根据 react-router-dom 文档配置好路由添加过渡动画使用

Android数据库Room的实际使用过程总结

《Android数据库Room的实际使用过程总结》这篇文章主要给大家介绍了关于Android数据库Room的实际使用过程,详细介绍了如何创建实体类、数据访问对象(DAO)和数据库抽象类,需要的朋友可以... 目录前言一、Room的基本使用1.项目配置2.创建实体类(Entity)3.创建数据访问对象(DAO

Spring Boot实现多数据源连接和切换的解决方案

《SpringBoot实现多数据源连接和切换的解决方案》文章介绍了在SpringBoot中实现多数据源连接和切换的几种方案,并详细描述了一个使用AbstractRoutingDataSource的实... 目录前言一、多数据源配置与切换方案二、实现步骤总结前言在 Spring Boot 中实现多数据源连接

Android WebView的加载超时处理方案

《AndroidWebView的加载超时处理方案》在Android开发中,WebView是一个常用的组件,用于在应用中嵌入网页,然而,当网络状况不佳或页面加载过慢时,用户可能会遇到加载超时的问题,本... 目录引言一、WebView加载超时的原因二、加载超时处理方案1. 使用Handler和Timer进行超

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影