1 蓝牙的两种类型

部署最为普遍的两种规格为蓝牙基础率/增强数据率 (BR/EDR)(采用版本为 2.0/2.1)和低耗能 (LE) 蓝牙(采用版本为 4.0/4.1/4.2)。

存在哪些差异?

蓝牙 BR/EDR—可建立相对较短距离的持续无线连接,因此非常适用于流式音频等应用

蓝牙 LE—可建立短时间的长距离无线电连接,非常适用于无需持续连接但依赖电池具有较长寿命的的物联网 (IoT) 应用

双模—双模芯片可支持需要连接 BR/EDR 设备(例如音频耳机)以及 LE 设备(例如穿戴设备或零售信标)的单一设备(例如智能手机或平板电脑)

2 核心系统结构

该系统包含射频收发器、基带和协议栈,支持设备连接和交换各类数据。

蓝牙设备交换根据蓝牙规格协议信号。核心系统协议包括射频 (RF) 协议、链路控制 (LC) 协议、链路管理器 (LM) 协议以及逻辑链路控制和适配协议 (L2CAP),蓝牙规格详细定义了这些协议。

最低的三个系统层—射频、链路控制和链路管理器协议—通常被归属于称为蓝牙控制器的子系统。这是一种采用可选标准接口—主机控制器接口 (HCI)—的通用部署,支持与蓝牙系统的其他设备(即蓝牙主机)进行双向通信。

主控制器可能是以下配置之一,具体取决于用例:

BR/EDR 控制器,包括射频、基带、链路管理器和可选 HCI

LE 控制器,包括 LE PHY、链路层和可选 HCI

BR/EDR 组合控制器和 LE 控制器,组合控制器共享一个蓝牙设备地址

蓝牙规格通过定义等效层之间交换的协议信息来实现系统之间的互操作性。它还通过定义蓝牙控制器和蓝牙主机之间的公用接口来实现独立蓝牙子系统之间的互操作性。

物理 (PHY) 层

通过蓝牙通信信道控制 2.4Ghz 射频的传输/接收。BR/EDR 提供的信道较多但带宽较窄,而 LE 使用的信道较少但带宽较宽。

链路层

定义数据包结构/信道、发现/连接程序以及发送/接收数据。

直接测试模式

允许测试人员向 PHY 层发出指令以传输或接收给定数据包序列,通过 HCI 或 2 线 UART 接口提交命令。

主机控制器接口 (HCI)

蓝牙控制器子系统(底部三层)和蓝牙主机之间的可选标准接口。

逻辑链路控制和适配协议 (L2CAP) 层

基于数据包的协议,可将数据包传输至 HCI 或直接传输到无主机系统中的链路管理器。支持更高级别的协议多路复用、数据包分割和重组,以及将服务质量信息传输到更高层。

属性协议 (ATT)

在建立连接之后定义数据交换客户端/服务器协议。使用通用属性配置文件 (GATT) 将属性分类为有意义的服务。ATT 主要用于 LE 部署,偶尔也会用于 BR/EDR 部署。

安全管理器

定义管理蓝牙设备之间配对完整性、身份验证以及加密的协议和操作,提供安全功能工具箱,其他组件可利用该工具箱支持不同应用所需的各种安全级别。

通用属性配置文件 (GATT)

使用属性协议,GATT 对封装设备组件性能的服务进行分组,并描述基于 GATT 功能的用例、角色和一般性能。其服务框架定义服务规程和格式及其特性,其中包括发现、读取、写入、通知以及指示特性以及配置特性广播。GATT 仅用于蓝牙 LE 部署。

通用访问配置文件(GAP)

可与蓝牙 LE 部署中的 GATT 配合使用,以定义与发现蓝牙设备和共享信息相关的规程和角色,以及连接蓝牙设备的链路管理内容。

3 Android抓取hci蓝牙log

本文是基于AOSP,android 6.0抓取蓝牙log方式描述。

在安卓手机设置-开发者选项-启用蓝牙HCI信息收集日志。个人理解我们抓取的日志实际上就是host和controller通过HCI收发数据截获的数据帧。

3.1 操作描述

1.捷波朗蓝牙耳机进入可被发现状态;

2.打开手机上的蓝牙开关;

3.扫描到蓝牙设备,直到扫描结束;

4.手机上点击捷波朗进行配对;

5.稍等片刻手机和捷波朗耳机配对并连接成功;

6.打开手机音乐播放器放音乐,耳机中听到音乐声,停留一小段时间;

7.手机中关闭蓝牙;

8.蓝牙耳机断开连接,并关闭耳机。

重启手机PC上可以在内置存储中找到btsnoop_hci.log文件,或者用其他第三方360助手等导出到PC,这就是蓝牙日志,PC上使用WireShark进行日志分析。

3.2 分析蓝牙log

对照上面的操作描述。

1.打开手机上的蓝牙开关,host向controller发送Reset指令,标志着即将启动手机上的蓝牙模块。

2.手机上的蓝牙开启成功以后,自动进入搜索模式,直到搜索结束,这个时候controller向host上报搜索结果,其中就有捷波朗蓝牙耳机,如下:

可以发现捷波朗耳机为可穿戴耳机设备(Wearable Headset Device),耳机名称缩写名称为Jabra EXTREME2,以及信号强度RSSI值等。

3.点击手机上的配对和耳机进行配对。

对照log可以知道:

首先会请求被连接设备的详细名称;

然后创建连接、获取时钟偏移等,具体如下;

接下来才是真正的匹配,进行身份认证;

4.手机和蓝牙耳机连接成功,配置完成后可正常传输数据。这里的主从角色和我们想当然的理解有差异,蓝牙耳机是主设备,手机是从设备。

5.手机上播放音乐,蓝牙耳机接收,并未看到熟悉的A2DP协议,实际上SBC就是A2DP中关于音频的编码格式。

SBC即Sub-band coding,子带编码,是A2DP(Advanced Audio Distribution Profile,蓝牙音频传输协议)协议强制规定的编码格式。

这个耳机比较老了,编码用的是SBC格式,新耳机目前会采用ACC、APTX和LDAC等格式编码,进一步提升音频品质。

6.关闭手机蓝牙,也会看到Reset命令。

以上就是对照log粗略分析蓝牙耳机从配对到连接,再到通过蓝牙接收手机音频的过程。

4. 手机通话过程中蓝牙耳机连接分析

4.1 预备知识

在主单元和从单元之间,可以确定不同的类型的蓝牙物理链路:ACL(Asynchronous Connectionless),和另一种链路是SCO(Synchronous Connection Oriented)。SCO主要用于同步话音传送,ACL主要用于分组数据传送。A2DP(Advanced Audio Distribution Profile 高级音频传输模型)是跑在ACL链路上的高品质音频协议。

SCO连接对称连接,利用保留时隙传送数据包。它主要用于:主单元和从单元之间实现点到点链接。连接建立后,主设备和从设备可以不被选中就发送SCO数据包。

(1) SCO数据包既可以传送话音,也可以传送数据,但在传送数据时,只用于重发被损坏的那部分的数据。

(2) SCO主要用来传输对时间要求很高的数据通信。

(3) SCO 链接由主单元发送SCO 建立消息,经链接管理(LM)协议来确立。

ACL链路就是定向发送数据包,它既支持对称连接,也支持不对称连接(既可以一对一,也可以一对多)。主要用于:主单元与网中的所有从单元之间实现一点多址的连接方式。

主设备负责控制链路带宽,并决定微微网中的每个从设备可以占用多少带宽和连接的对称性。从设备只有被选中时才能传送数据。ACL链路也支持接收主设备发给微微网中所有从设备的广播消息。

4.2 蓝牙耳机通话分析

(1) 模式设置

为指定连接句柄写链路策略设置。链路策略设置允许主机控制器指定用于连接句柄的LM连接模式

host->controlller

Frame 321: 8 bytes on wire (64 bits), 8 bytes captured (64 bits)
Bluetooth[Source: host][Destination: controller]
Bluetooth HCI H4[Direction: Sent (0x00)]HCI Packet Type: HCI Command (0x01)
Bluetooth HCI Command - Write Link Policy SettingsCommand Opcode: Write Link Policy Settings (0x080d)Parameter Total Length: 4Connection Handle: 0x0003.... .... .... ...1 = Enable Master Slave Switch: true (1)  //允许角色切换.... .... .... ..1. = Enable Hold Mode: true (1)    //改变LM状态和本地及远程设备为主模式.... .... .... .1.. = Enable Sniff Mode: true (1)   //改变LM状态和本地及远程设备为呼吸模式.... .... .... 0... = Enable Park Mode: false (0)   //改变LM状态和本地及远程设备为休眠模式[Response in frame: 322][Command-Response Delta: 1.3ms]

controlller->host

Frame 322: 9 bytes on wire (72 bits), 9 bytes captured (72 bits)
Bluetooth[Source: controller][Destination: host]
Bluetooth HCI H4[Direction: Rcvd (0x01)]HCI Packet Type: HCI Event (0x04)
Bluetooth HCI Event - Command CompleteEvent Code: Command Complete (0x0e)//命令执行状态-完成Parameter Total Length: 6Number of Allowed Command Packets: 1Command Opcode: Write Link Policy Settings (0x080d)0000 10.. .... .... = Opcode Group Field: Link Policy Commands (0x02).... ..00 0000 1101 = Opcode Command Field: Write Link Policy Settings (0x00d)Status: Success (0x00)Connection Handle: 0x0003 //建立连接句柄[Command in frame: 321][Command-Response Delta: 1.3ms]

(2) 语音设置

host->controlller

Frame 1471: 6 bytes on wire (48 bits), 6 bytes captured (48 bits)
Bluetooth[Source: host][Destination: controller]
Bluetooth HCI H4[Direction: Sent (0x00)]HCI Packet Type: HCI Command (0x01)
Bluetooth HCI Command - Write Voice SettingCommand Opcode: Write Voice Setting (0x0c26)Parameter Total Length: 20000 00.. .... .... = Unused bits: 0x00.... ..00 .... .... = Input Coding: Linear (0).... .... 01.. .... = Input Data Format: 2's complement (1).... .... ..1. .... = Input Sample Size: 16 bit (only for Linear PCM) (1).... .... ...0 00.. = Linear PCM Bit Position: 0.... .... .... ..11 = Air Coding Format: Transparent (3)[Response in frame: 1472][Command-Response Delta: 0.36ms]

controlller->host

Frame 1472: 7 bytes on wire (56 bits), 7 bytes captured (56 bits)
Bluetooth[Source: controller][Destination: host]
Bluetooth HCI H4[Direction: Rcvd (0x01)]HCI Packet Type: HCI Event (0x04)
Bluetooth HCI Event - Command CompleteEvent Code: Command Complete (0x0e)Parameter Total Length: 4Number of Allowed Command Packets: 1Command Opcode: Write Voice Setting (0x0c26)0000 11.. .... .... = Opcode Group Field: Host Controller & Baseband Commands (0x03).... ..00 0010 0110 = Opcode Command Field: Write Voice Setting (0x026)Status: Success (0x00)[Command in frame: 1471][Command-Response Delta: 0.36ms]

(3) 建立SCO连接

host->controlller

Frame 1473: 21 bytes on wire (168 bits), 21 bytes captured (168 bits)
Bluetooth[Source: host][Destination: controller]
Bluetooth HCI H4[Direction: Sent (0x00)]HCI Packet Type: HCI Command (0x01)
Bluetooth HCI Command - Setup Synchronous ConnectionCommand Opcode: Setup Synchronous Connection (0x0428)Parameter Total Length: 17Connection Handle: 0x0003Tx Bandwidth (bytes/s): 8000Rx Bandwidth (bytes/s): 8000Max. Latency (ms): 13//语音设置参数,可以理解为SCO通道的属性0000 00.. .... .... = Unused bits: 0x00.... ..00 .... .... = Input Coding: Linear (0).... .... 01.. .... = Input Data Format: 2's complement (1).... .... ..1. .... = Input Sample Size: 16 bit (only for Linear PCM) (1).... .... ...0 00.. = Linear PCM Bit Position: 0.... .... .... ..11 = Air Coding Format: Transparent (3)Retransmission Effort: At least 1 retransmission, optimize for link quality (2)Packet Type: 0x0388, 3-EV5, 2-EV5, 3-EV3, EV30000 00.. .... .... = Reserved: 0x00.... ..1. .... .... = 3-EV5: True.... ...1 .... .... = 2-EV5: True.... .... 1... .... = 3-EV3: True.... .... .0.. .... = 2-EV3: False.... .... ..0. .... = EV5: False.... .... ...0 .... = EV4: False.... .... .... 1... = EV3: True.... .... .... .0.. = HV3: False.... .... .... ..0. = HV2: False.... .... .... ...0 = HV1: False[Pending in frame: 1474][Command-Pending Delta: 1.161ms][Response in frame: 1476][Command-Response Delta: 201.225ms]

controlller->host

Frame 1474: 7 bytes on wire (56 bits), 7 bytes captured (56 bits)
Bluetooth[Source: controller][Destination: host]
Bluetooth HCI H4[Direction: Rcvd (0x01)]HCI Packet Type: HCI Event (0x04)
Bluetooth HCI Event - Command StatusEvent Code: Command Status (0x0f)Parameter Total Length: 4Status: Pending (0x00)Number of Allowed Command Packets: 1Command Opcode: Setup Synchronous Connection (0x0428)0000 01.. .... .... = Opcode Group Field: Link Control Commands (0x01).... ..00 0010 1000 = Opcode Command Field: Setup Synchronous Connection (0x028)[Command in frame: 1473][Response in frame: 1476][Command-Pending Delta: 1.161ms][Pending-Response Delta: 200.064ms]
Frame 1476: 20 bytes on wire (160 bits), 20 bytes captured (160 bits)
Bluetooth[Source: controller][Destination: host]
Bluetooth HCI H4[Direction: Rcvd (0x01)]HCI Packet Type: HCI Event (0x04)
Bluetooth HCI Event - Synchronous Connection CompleteEvent Code: Synchronous Connection Complete (0x2c)Parameter Total Length: 17Status: Success (0x00)//成功Connection Handle: 0x0004 //建立连接句柄BD_ADDR: GnNetcom_83:4e:ce (50:c9:71:83:4e:ce)Link Type: eSCO connection (0x02)Transmit Interval: 12 slots (7.5 msec)Retransmit Window: 4 slots (2.5 msec)Rx Packet Length: 60Tx Packet Length: 60Air Mode: Transparent Data (3)[Command in frame: 1473][Pending in frame: 1474][Pending-Response Delta: 200.064ms][Command-Response Delta: 201.225ms]

(4) 打电话,低功耗监听模式(Sniff Subrating)、呼吸模式(Sniff Mode)设置

(5) 结束电话断开SCO连接

host->controller

Frame 1490: 7 bytes on wire (56 bits), 7 bytes captured (56 bits)
Bluetooth[Source: host][Destination: controller]
Bluetooth HCI H4[Direction: Sent (0x00)]HCI Packet Type: HCI Command (0x01)
Bluetooth HCI Command - DisconnectCommand Opcode: Disconnect (0x0406)Parameter Total Length: 3Connection Handle: 0x0004 //注意连接句柄和建立sco通道时候一致Reason: Remote User Terminated Connection (0x13)[Pending in frame: 1491][Command-Pending Delta: 4.262ms][Response in frame: 1492][Command-Response Delta: 97.828ms]

controlller->host

Frame 1491: 7 bytes on wire (56 bits), 7 bytes captured (56 bits)
Bluetooth[Source: controller][Destination: host]
Bluetooth HCI H4[Direction: Rcvd (0x01)]HCI Packet Type: HCI Event (0x04)
Bluetooth HCI Event - Command StatusEvent Code: Command Status (0x0f)Parameter Total Length: 4Status: Pending (0x00)Number of Allowed Command Packets: 1Command Opcode: Disconnect (0x0406)0000 01.. .... .... = Opcode Group Field: Link Control Commands (0x01).... ..00 0000 0110 = Opcode Command Field: Disconnect (0x006)[Command in frame: 1490][Response in frame: 1492][Command-Pending Delta: 4.262ms][Pending-Response Delta: 93.566ms]
Frame 1492: 7 bytes on wire (56 bits), 7 bytes captured (56 bits)
Bluetooth[Source: controller][Destination: host]
Bluetooth HCI H4[Direction: Rcvd (0x01)]HCI Packet Type: HCI Event (0x04)
Bluetooth HCI Event - Disconnect CompleteEvent Code: Disconnect Complete (0x05) //断开连接成功Parameter Total Length: 4Status: Success (0x00)Connection Handle: 0x0004Reason: Connection Terminated by Local Host (0x16)[Command in frame: 1490][Pending in frame: 1491][Pending-Response Delta: 93.566ms][Command-Response Delta: 97.828ms]

5.手机通过蓝牙耳机录音解决办法

经过查阅资料可以知道大部分手机并不支持蓝牙耳机录音功能,但我们在通话过程中使用蓝牙耳机的确可以正常输入语音的,那么可以推断想要通过蓝牙耳机录音是不是需要在录音APP中开启相应的服务支持。

其实经过上面的分析已经知道打电话之所以可以通过蓝牙输入和输出声音,实际上是通过建立SCO连接完成的,所以需要在APP代码内控制SCO连接。也就是说想要使用APP蓝牙录音,也要建立SCO连接。是不是确实建立成功可以通过蓝牙HCI log分析确定。

先来看Android 6.0蓝牙架构

蓝牙系统服务通过JNI与蓝牙协议栈,通过Binder IPC和应用进行通信。系统服务为开发人员提供了访问各种蓝牙配置文件的机会。

电话中是如何建立SCO连接的?

通过电话接通后,选择蓝牙音频输出入口

packages/apps/InCallUI/src/com/android/incallui/CallButtonFragment.java

    @Overridepublic boolean onMenuItemClick(MenuItem item) {Log.d(this, "- onMenuItemClick: " + item);Log.d(this, "  id: " + item.getItemId());Log.d(this, "  title: '" + item.getTitle() + "'");int mode = CallAudioState.ROUTE_WIRED_OR_EARPIECE;switch (item.getItemId()) {case R.id.audio_mode_speaker:mode = CallAudioState.ROUTE_SPEAKER;break;case R.id.audio_mode_earpiece:case R.id.audio_mode_wired_headset:// InCallCallAudioState.ROUTE_EARPIECE means either the handset earpiece,// or the wired headset (if connected.)mode = CallAudioState.ROUTE_WIRED_OR_EARPIECE;break;case R.id.audio_mode_bluetooth:mode = CallAudioState.ROUTE_BLUETOOTH;break;default:Log.e(this, "onMenuItemClick:  unexpected View ID " + item.getItemId()+ " (MenuItem = '" + item + "')");break;}getPresenter().setAudioMode(mode);return true;}

看到了MVP模式中的P,通过点击菜单上不同的选项调用setAudioMode(mode),设置音频模式

packages/apps/InCallUI/src/com/android/incallui/CallButtonPresenter.java

public void setAudioMode(int mode) {// TODO: Set a intermediate state in this presenter until we get// an update for onAudioMode().  This will make UI response immediate// if it turns out to be slowLog.d(this, "Sending new Audio Mode: " + CallAudioState.audioRouteToString(mode));TelecomAdapter.getInstance().setAudioRoute(mode);}

进入TelecomAdapter单例,设置音频路由,发现实际是调用InCallService的setAudioRoute(route)方法

packages/apps/InCallUI/src/com/android/incallui/TelecomAdapter.java

void setAudioRoute(int route) {if (mInCallService != null) {mInCallService.setAudioRoute(route);} else {Log.e(this, "error setAudioRoute, mInCallService is null");}}

实际是调用Phone的setAudioRoute(route)方法

frameworks/base/telecomm/java/android/telecom/InCallService.java

/*** This service is implemented by any app that wishes to provide the user-interface for managing* phone calls. Telecom binds to this service while there exists a live (active or incoming) call,* and uses it to notify the in-call app of any live and recently disconnected calls. An app must* first be set as the default phone app (See {@link TelecomManager#getDefaultDialerPackage()})* before the telecom service will bind to its {@code InCallService} implementation.* <p>* Below is an example manifest registration for an {@code InCallService}. The meta-data* ({@link TelecomManager#METADATA_IN_CALL_SERVICE_UI}) indicates that this particular* {@code InCallService} implementation intends to replace the built-in in-call UI.* <pre>* {@code* &lt;service android:name="your.package.YourInCallServiceImplementation"*          android:permission="android.permission.BIND_IN_CALL_SERVICE"&gt;*      &lt;meta-data android:name="android.telecom.IN_CALL_SERVICE_UI" android:value="true" /&gt;*      &lt;intent-filter&gt;*          &lt;action android:name="android.telecom.InCallService"/&gt;*      &lt;/intent-filter&gt;* &lt;/service&gt;* }* </pre>*/
public abstract class InCallService extends Service {.../*** Sets the audio route (speaker, bluetooth, etc...).  When this request is honored, there will* be change to the {@link #getCallAudioState()}.** @param route The audio route to use.*/public final void setAudioRoute(int route) {if (mPhone != null) {mPhone.setAudioRoute(route);}}...
}

frameworks/base/telecomm/java/android/telecom/Phone.java

    /*** Sets the audio route (speaker, bluetooth, etc...).  When this request is honored, there will* be change to the {@link #getAudioState()}.** @param route The audio route to use.*/public final void setAudioRoute(int route) {mInCallAdapter.setAudioRoute(route);}

实际是使用InCallAdapter的setAudioRoute(route)方法

frameworks/base/telecomm/java/android/telecom/InCallAdapter.java

    /*** Sets the audio route (speaker, bluetooth, etc...). See {@link CallAudioState}.** @param route The audio route to use.*/public void setAudioRoute(int route) {try {mAdapter.setAudioRoute(route);} catch (RemoteException e) {}}

android.telecom.InCallAdapter实际使用了com.android.server.telecom.InCallAdapter的setAudioRoute(route)

frameworks/base/telecomm/java/com/android/internal/telecom/IInCallAdapter.aidl

/*** Internal remote callback interface for in-call services.** @see android.telecom.InCallAdapter** {@hide}*/
oneway interface IInCallAdapter {...void setAudioRoute(int route);...
}

packages/services/Telecomm/src/com/android/server/telecom/InCallAdapter.java

/*** Receives call commands and updates from in-call app and passes them through to CallsManager.* {@link InCallController} creates an instance of this class and passes it to the in-call app after* binding to it. This adapter can receive commands and updates until the in-call app is unbound.*/
class InCallAdapter extends IInCallAdapter.Stub {...@Overridepublic void setAudioRoute(int route) {long token = Binder.clearCallingIdentity();try {synchronized (mLock) {mCallsManager.setAudioRoute(route);}} finally {Binder.restoreCallingIdentity(token);}}...
}

packages/services/Telecomm/src/com/android/server/telecom/CallsManager.java

    /*** Called by the in-call UI to change the audio route, for example to change from earpiece to* speaker phone.*/void setAudioRoute(int route) {mCallAudioManager.setAudioRoute(route);}

packages/services/Telecomm/src/com/android/server/telecom/CallAudioManager.java

    /*** Changed the audio route, for example from earpiece to speaker phone.** @param route The new audio route to use. See {@link CallAudioState}.*/void setAudioRoute(int route) {// This can happen even when there are no calls and we don't have focus.if (!hasFocus()) {return;}Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));// Change ROUTE_WIRED_OR_EARPIECE to a single entry.int newRoute = selectWiredOrEarpiece(route, mCallAudioState.getSupportedRouteMask());// If route is unsupported, do nothing.if ((mCallAudioState.getSupportedRouteMask() | newRoute) == 0) {Log.wtf(this, "Asking to set to a route that is unsupported: %d", newRoute);return;}if (mCallAudioState.getRoute() != newRoute) {// Remember the new speaker state so it can be restored when the user plugs and unplugs// a headset.mWasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER;setSystemAudioState(mCallAudioState.isMuted(), newRoute,mCallAudioState.getSupportedRouteMask());}}private void setSystemAudioState(boolean isMuted, int route, int supportedRouteMask) {setSystemAudioState(false /* force */, isMuted, route, supportedRouteMask);}private void setSystemAudioState(boolean force, boolean isMuted, int route, int supportedRouteMask) {if (!hasFocus()) {return;}CallAudioState oldAudioState = mCallAudioState;saveAudioState(new CallAudioState(isMuted, route, supportedRouteMask));if (!force && Objects.equals(oldAudioState, mCallAudioState)) {return;}Log.i(this, "setSystemAudioState: changing from %s to %s", oldAudioState, mCallAudioState);Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,CallAudioState.audioRouteToString(mCallAudioState.getRoute()));mAudioManagerHandler.obtainMessage(MSG_AUDIO_MANAGER_SET_MICROPHONE_MUTE,mCallAudioState.isMuted() ? 1 : 0,0).sendToTarget();// Audio route.if (mCallAudioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) {turnOnSpeaker(false);turnOnBluetooth(true);} else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {turnOnBluetooth(false);turnOnSpeaker(true);} else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE ||mCallAudioState.getRoute() == CallAudioState.ROUTE_WIRED_HEADSET) {turnOnBluetooth(false);turnOnSpeaker(false);}if (!oldAudioState.equals(mCallAudioState)) {mCallsManager.onCallAudioStateChanged(oldAudioState, mCallAudioState);updateAudioForForegroundCall();}}
  • 分析到这里看到了关于切换蓝牙输出的代码:
if (mCallAudioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) {turnOnSpeaker(false);turnOnBluetooth(true);
}

赶紧看看turnOnBluetooth(true)方法,这一定是打开蓝牙语音的代码

    private void turnOnBluetooth(boolean on) {if (mBluetoothManager.isBluetoothAvailable()) {boolean isAlreadyOn = mBluetoothManager.isBluetoothAudioConnectedOrPending();if (on != isAlreadyOn) {Log.i(this, "connecting bluetooth %s", on);if (on) {mBluetoothManager.connectBluetoothAudio();} else {mBluetoothManager.disconnectBluetoothAudio();}}}}

先判断蓝牙设备是否存在,是否已经使用蓝牙设备作为输出,如果不是才进行切换。在此进入了BluetoothManager管辖范围

packages/services/Telecomm/src/com/android/server/telecom/BluetoothManager.java

    void connectBluetoothAudio() {Log.v(this, "connectBluetoothAudio()...");if (mBluetoothHeadset != null) {mBluetoothHeadset.connectAudio();}// Watch out: The bluetooth connection doesn't happen instantly;// the connectAudio() call returns instantly but does its real// work in another thread.  The mBluetoothConnectionPending flag// is just a little trickery to ensure that the onscreen UI updates// instantly. (See isBluetoothAudioConnectedOrPending() above.)mBluetoothConnectionPending = true;mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();}

这个方法实际是调用BluetoothHeadset对象的connectAudio()方法,启动一个蓝牙耳机连接,使用SCO信道,到此已经知道电话切换蓝牙音频输出确实是用蓝牙SCO信道实现的。

frameworks/base/core/java/android/bluetooth/BluetoothHeadset.java

    /*** Initiates a connection of headset audio.* It setup SCO channel with remote connected headset device.** @return true if successful*         false if there was some error such as*               there is no connected headset* @hide*/public boolean connectAudio() {if (mService != null && isEnabled()) {try {return mService.connectAudio();} catch (RemoteException e) {Log.e(TAG, e.toString());}} else {Log.w(TAG, "Proxy not attached to service");if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));}return false;}

frameworks/base/core/java/android/bluetooth/IBluetoothHeadset.aidl

/*** API for Bluetooth Headset service** {@hide}*/
interface IBluetoothHeadset {// Public API...boolean connectAudio();...
}

BluetoothManager中调用connectBluetoothAudio(),实际上是远程调用HeadsetService服务中的connectAudio()

packages/apps/Bluetooth/src/com/android/bluetooth/hfp/HeadsetService.java

public boolean connectAudio() {  HeadsetService service = getService();  if (service == null) return false;  return service.connectAudio();
}
    boolean connectAudio() {  // TODO(BT) BLUETOOTH or BLUETOOTH_ADMIN permission  enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");  if (!mStateMachine.isConnected()) {  return false;  }  if (mStateMachine.isAudioOn()) {  return false;  }  mStateMachine.sendMessage(HeadsetStateMachine.CONNECT_AUDIO);  return true;  }

先检查权限BLUETOOTH或者BLUETOOTH_ADMIN,然后判断连接状态,未连接和音频已经通过蓝牙输出都返回false,什么都没做。只有在已连接蓝牙耳机但还没使用蓝牙耳机作为输出的时候才去发出Message,从HeadsetStateMachine命名也可以知道它是状态机,当接收到CONNECT_AUDIO命令时就会将蓝牙状态切换为AudioOn

packages/apps/Bluetooth/src/com/android/bluetooth/hfp/HeadsetStateMachine.java

/*** Bluetooth Handset StateMachine*                      (Disconnected)*                           |    ^*                   CONNECT |    | DISCONNECTED*                           V    |*                         (Pending)*                           |    ^*                 CONNECTED |    | CONNECT*                           V    |*                        (Connected)*                           |    ^*             CONNECT_AUDIO |    | DISCONNECT_AUDIO*                           V    |*                         (AudioOn)*/

从分发的消息分支很容易找到下面的方法

connectAudioNative(getByteAddress(device));

进入了Native层

private native boolean connectAudioNative(byte[] address);

packages/apps/Bluetooth/jni/com_android_bluetooth_hfp.cpp

static jboolean connectAudioNative(JNIEnv *env, jobject object, jbyteArray address) {jbyte *addr;bt_status_t status;if (!sBluetoothHfpInterface) return JNI_FALSE;addr = env->GetByteArrayElements(address, NULL);if (!addr) {jniThrowIOException(env, EINVAL);return JNI_FALSE;}if ( (status = sBluetoothHfpInterface->connect_audio((bt_bdaddr_t *)addr)) !=BT_STATUS_SUCCESS) {ALOGE("Failed HF audio connection, status: %d", status);}env->ReleaseByteArrayElements(address, addr, 0);return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
}

分析到这里关键在sBluetoothHfpInterface->connect_audio((bt_bdaddr_t *)addr))这个方法调用,进入到了蓝牙协议栈

system/bt/btif/src/btif_hf.c

/*******************************************************************************
**
** Function         connect_audio
**
** Description     create an audio connection
**
** Returns         bt_status_t
**
*******************************************************************************/
static bt_status_t connect_audio( bt_bdaddr_t *bd_addr )
{CHECK_BTHF_INIT();int idx = btif_hf_idx_by_bdaddr(bd_addr);if ((idx < 0) || (idx >= BTIF_HF_NUM_CB)){BTIF_TRACE_ERROR("%s: Invalid index %d", __FUNCTION__, idx);return BT_STATUS_FAIL;}/* Check if SLC is connected */if (btif_hf_check_if_slc_connected() != BT_STATUS_SUCCESS)return BT_STATUS_NOT_READY;if (is_connected(bd_addr) && (idx != BTIF_HF_INVALID_IDX)){BTA_AgAudioOpen(btif_hf_cb[idx].handle);/* Inform the application that the audio connection has been initiated successfully */btif_transfer_context(btif_in_hf_generic_evt, BTIF_HFP_CB_AUDIO_CONNECTING,(char *)bd_addr, sizeof(bt_bdaddr_t), NULL);return BT_STATUS_SUCCESS;}return BT_STATUS_FAIL;
}

system/bt/bta/ag/bta_ag_api.c

/*******************************************************************************
**
** Function         BTA_AgAudioOpen
**
** Description      Opens an audio connection to the currently connected
**                  headset or hnadsfree.
**
**
** Returns          void
**
*******************************************************************************/
void BTA_AgAudioOpen(UINT16 handle)
{BT_HDR  *p_buf;if ((p_buf = (BT_HDR *) GKI_getbuf(sizeof(BT_HDR))) != NULL){p_buf->event = BTA_AG_API_AUDIO_OPEN_EVT;p_buf->layer_specific = handle;bta_sys_sendmsg(p_buf);}
}

system/bt/stack/include/bt_types.h

/* Define the header of each buffer used in the Bluetooth stack.*/
typedef struct
{uint16_t          event;uint16_t          len;uint16_t          offset;uint16_t          layer_specific;uint8_t           data[];
} BT_HDR;

system/bt/bta/sys/bta_sys_main.c

/*******************************************************************************
**
** Function         bta_sys_sendmsg
**
** Description      Send a GKI message to BTA.  This function is designed to
**                  optimize sending of messages to BTA.  It is called by BTA
**                  API functions and call-in functions.
**
**
** Returns          void
**
*******************************************************************************/
void bta_sys_sendmsg(void *p_msg)
{// There is a race condition that occurs if the stack is shut down while// there is a procedure in progress that can schedule a task via this// message queue. This causes |btu_bta_msg_queue| to get cleaned up before// it gets used here; hence we check for NULL before using it.if (btu_bta_msg_queue)fixed_queue_enqueue(btu_bta_msg_queue, p_msg);
}
  • system/bt/osi/src/fixed_queue.c
void fixed_queue_enqueue(fixed_queue_t *queue, void *data) {assert(queue != NULL);assert(data != NULL);semaphore_wait(queue->enqueue_sem);pthread_mutex_lock(&queue->lock);list_append(queue->list, data);pthread_mutex_unlock(&queue->lock);semaphore_post(queue->dequeue_sem);
}

后续进入到了linux内核,后续步骤可以猜得到,经过蓝牙驱动让蓝牙模块Host下发指令:Setup Synchronous Connection

APP建立SCO信道控制代码

实现蓝牙耳机录音功能

打开sco,关键代码:

AudioManager mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
mAudioManager.startBluetoothSco();

关闭sco:

mAudioManager.stopBluetoothSco();
  • 1

参考资料:

1.https://zhuanlan.zhihu.com/p/21943377

2.http://blog.csdn.net/xubin341719/article/details/38305331

3.http://blog.csdn.net/vnanyesheshou/article/details/71374935

4.http://blog.csdn.net/aaa111/article/details/50364061

5.http://androidxref.com

Android APP通过蓝牙耳机录音可行性分析相关推荐

  1. Android AOSP 6.0.1 APP通过蓝牙耳机录音可行性分析

    1 蓝牙的两种类型 部署最为普遍的两种规格为蓝牙基础率/增强数据率 (BR/EDR)(采用版本为 2.0/2.1)和低耗能 (LE) 蓝牙(采用版本为 4.0/4.1/4.2). 存在哪些差异? 蓝牙 ...

  2. Android蓝牙耳机录音

    Android蓝牙耳机录音 使用蓝牙耳机录音都需要开启耳机的SCO连接,这是一种双向语音通信的连接,开启蓝牙耳机的SCO连接有两种方式,一种是startBluetoothSco()的方式,另一种是st ...

  3. android蓝牙耳机录音播放,android蓝牙耳机录音并播放(二)

    Github下载 package com.example.superb.yy4; import android.content.Context; import android.media.AudioF ...

  4. 蓝牙耳机录音进行讯飞语音识别

    最近在做一个翻译戒指的项目 ,里面有语音识别,然后拿到识别的文字去翻译内容 语音识别,肯定是用讯飞的 ,但是项目的的声音来源不是手机 ,是蓝牙耳机 ,也是第一次接手这样的项目,有点蒙蔽了,去网上找了一 ...

  5. Android RecognizerIntent与蓝牙耳机

     http://codego.net/554937/ 使用Android RecognizerIntent与蓝牙耳机 + androidbluetooth-蓝牙speech-recognition ...

  6. Android 音频 OpenSL ES 录音 采集

    1,创建引擎 2,创建AudioRecorder并开始录音 3,暂停录音 4,释放资源 5,数据是通过回调函数处理的. 好处:缓冲区不用通过AudioRecord.getMinBufferSize获取 ...

  7. Android开发学习之录音同步播放的实现

    最近看到一篇关于音频的文章,忽然想起以前有个中国传媒大学的一位朋友,要我帮她设计一个可以实时播放输入音频的程序,我当时想到了要用DirectSound,可是对于这种从来没有碰过的东西,我内心是多少有些 ...

  8. Android 仿微信语音录音小控件

    Android 仿微信语音录音小控件 前段时间一直在做IM聊天这方面的撸码工作,涉及到了很多小控件,有时间我会慢慢给大家分享一下我的小成果,希望大家一起学习,一起进步,今天来和大家来说一下Androi ...

  9. 【Android App】在线语音识别功能实现(使用云知声平台与WebSocket 超详细 附源码)

    需要源码和相关资源请点赞关注收藏后评论区留下QQ~~~ 一.在线语音识别 云知声的语音识别同样采用WebSocket接口,待识别的音频流支持MP3和PCM两种格式,对于在线语音识别来说,云知声使用JS ...

最新文章

  1. 分布式一致性算法Raft
  2. sql and和in能连用吗_解析法律英语中【同义词连用】现象
  3. 猫头鹰的深夜翻译:API网关的重要性
  4. es重建字段类型_关于elasticsearch中更新数据的几种方式
  5. Python之nyoka:nyoka库函数的简介、安装、使用方法之详细攻略
  6. minGW, cygwin, GnuWin32【C++的跨平台交叉编译问题】
  7. ctf php文件上传图片格式,CTF-WEB:文件上传
  8. 利用Split函数进行多关键字检索
  9. kopernio显示无效程序_daz 无法渲染/没有渲染/渲染不显示/渲染无效?
  10. 大话Neo系列:Merkle Tree
  11. 别轻易接受父母的建议
  12. java时间轮定时器_基于时间轮的定时器
  13. 数据分析EXCEL常用统计函数
  14. 计算机网络技术ui设计,UI设计小白到大神的进阶之路—入门基础篇
  15. 【Coding】LSF作业系统查看bsub提交历史
  16. 编写程序,生成一种贯穿10×10字符数组(初始时全为字符‘.‘)的“随机步法”。
  17. echarts实现地图飞线
  18. Qt5操作Excel操作的一些心得
  19. 笔记13:Python 和 Elasticsearch 构建简易搜索
  20. JavaScript:表格生成器

热门文章

  1. 基于FS4412的学习实验二
  2. 基于神经网络进行数据降维
  3. qt交叉编译报错::-1: error: arm-linux-g++: Command not found
  4. Jupyter Notebook切换python运行环境
  5. 收拾好行囊 整理好心情 追梦
  6. react点击事件onClick
  7. matlab关于colorbar的整理(绘制不等间距colorbar, colorbar的大小位置调节, colorbar加单位等)
  8. Go社区主流Kakfa客户端简要对比
  9. Linux SCP命令使用记录
  10. 中创向心力:职业院校如何严格职业技能等级考核与证书发放?