基于Android Q的蓝牙通话无声问题

article/2025/6/19 15:11:16

分享一下这几天改蓝牙通话无声的bug,文章有点长,希望各位看官看完能帮助到大家—今天分析的主题是蓝牙通话没有声音之运行流程分析

一. 结果说在前面

​ 蓝牙通话分别有七个阶段,基本上每个阶段都会走到底层,把数据回调到上层,这么一个流程叫一个阶段;回调的数据代表一个状态,而我这个问题就在于底层回调数据到上层时,携带的数据不满足上层的要求 导致无法进入下一个阶段而产生的蓝牙通话无声问题;

​ 蓝牙通话的七个连接阶段使用的模式是:状态机模式,没弄明白状态机模式之前有点不好入手;我这里分享一下状态机模式的详解文章:状态机模式介绍

二.涉及到的文件/内容

  1. vendor/mediatek/proprietary/packages/apps/Bluetooth/src/com/android/bluetooth/hfp/HeadsetNativeInterface.java

  2. vendor/mediatek/proprietary/packages/apps/Bluetooth/src/com/android/bluetooth/hfp/HeadsetStateMachine.java

  3. vendor/mediatek/proprietary/packages/apps/Bluetooth/src/com/android/bluetooth/hfp/HeadsetService.java

  4. frameworks/base/services/core/java/com/android/server/audio/BtHelper.java

    蓝牙服务类别:

  5. HeadsetPro-file(HSP)代表耳机功能,提供手机与耳机之间通信所需的基本功能。

  6. HandProfile(HFP)则代表免提功能,HFP在HSP的基础上增加了某些扩展功能。

  7. Advanced Audio Distribution Profile(A2DP),指的是 蓝牙音频传输模型协定。

  8. 蓝牙物理链路SCO(Synchronous Connection Oriented)主要用来传输对时间要求很高的数据通信,通常用于通话

三.蓝牙连接状态分析

1.连接状态分析

​ 蓝牙连接步骤分为七个状态,分别是:

  1. Disconnected

    该状态为初始化阶段,在蓝牙打开阶段会执行

  2. Connecting

    该状态在连接蓝牙时执行

  3. Connected

    该状态在蓝牙连接完成后执行

  4. AudioConnecting

    该状态在接听电话后执行,处于一个连接中的一个状态,很快就能连接上

  5. AudioOn

    该状态在接听电话后,AudioConnecting阶段完成后执行,这个时候通常就能用蓝牙耳机通话了

  6. AudioDiconnecting

    该状态在挂断电话后执行,此时又回到了Connected状态,因为蓝牙还是开启的

  7. Disconnecting

    该状态在蓝牙断开中执行,执行完成后回到Disconnected该状态

以上就是蓝牙连接到蓝牙断开的七个连接状态,每个状态都继承了状态机,接下来看看每个连接状态的具体方法,具体实现稍后分析

2.状态机方法

​ 每个连接状态都有一个状态模式,都分别继承于HeadsetStateBaseConnectedBase,它们共同的父父类就是State,先来看一段代码,七个连接状态

2.1 HeadsetStateMachine.java

 // State machine statesprivate final Disconnected mDisconnected = new Disconnected();private final Connecting mConnecting = new Connecting();private final Disconnecting mDisconnecting = new Disconnecting();private final Connected mConnected = new Connected();private final AudioConnecting mAudioConnecting = new AudioConnecting();private final AudioOn mAudioOn = new AudioOn();private final AudioDisconnecting mAudioDisconnecting = new AudioDisconnecting();
... // Initialize state machineaddState(mDisconnected);addState(mConnecting);addState(mDisconnecting);addState(mConnected);addState(mAudioOn);addState(mAudioConnecting);addState(mAudioDisconnecting);setInitialState(mDisconnected);

分别把七个连接阶段加入到状态机中,初始化mDisconnected为默认状态,每个状态机都有四个方法,具体看代码

    class 七个连接状态 extends ConnectedBase 或者 HeadsetStateBase {@Overrideint getAudioStateInt() {...}@Overridepublic void enter() {...}// ... @Overridepublic boolean processMessage(Message message) {...}@Overridepublic void exit() {...}}

这三个方法是每个连接阶段必有的,getAudioStateInt用于一个常量代表当前返回的状态,enter进入该方法,processMessage事件逻辑处理,exit退出当前状态,这四个方法是顺序执行。

3.连接状态具体实现

之前说道基本上每个连接状态都会走到底层拿数据回调上层,这里我们来具体看看,主要分析AudioConnecting阶段,大致分析其他阶段

3.1 Disconnected

 class Disconnected extends HeadsetStateBase {...@Overridepublic boolean processMessage(Message message) {...if (!mNativeInterface.connectHfp(device)) { // 第一步stateLogE("CONNECT failed for connectHfp(" + device + ")");// No state transition is involved, fire broadcast immediatelybroadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,BluetoothProfile.STATE_DISCONNECTED);break;}transitionTo(mConnecting); // 第二步...return HANDLED;}   ...}

很关键的两步,第一步连接之前提到过的HFP服务,调用底层,连接成功后发送广播;第二步状态切换,进入下一步连接状态;这里的返回值一定要是true才行,才能进入exit,HANDLED默认就是true;底层回调:HeadsetNativeInterface.java # private native boolean connectHfpNative(byte[] address);

3.2 Connecting

class Connecting extends HeadsetStateBase {...@Overridepublic void processConnectionEvent(Message message, int state) {switch (state) {...case HeadsetHalConstants.CONNECTION_STATE_SLC_CONNECTED://走了一系列流程后进入该casestateLogD("SLC connected");transitionTo(mConnected);//调用该方法进入下一个阶段break;...}} ...}

这里的processConnectionEvent方法的state值是从HeadsetNativeInterface#onConnectionStateChanged开始调用底层,携带state数据,再回调上层来,接着再根据state来走case是否进入mConnected阶段

每个阶段差不多都是这样的流程,这里就不一一去分析了,直接看AudioConnecting阶段

3.3 AudioConnecting

这个阶段就是我遇到问题的一个阶段,由于state值导致无法进入下一阶段,上代码

     //各阶段常量static final int CONNECT = 1;static final int DISCONNECT = 2;static final int CONNECT_AUDIO = 3;//注意static final int DISCONNECT_AUDIO = 4;class AudioConnecting extends ConnectedBase {@Overrideint getAudioStateInt() {return BluetoothHeadset.STATE_AUDIO_CONNECTING; //记住这段代码,它的值为11}@Overridepublic void enter() {super.enter();sendMessageDelayed(CONNECT_TIMEOUT, mDevice, sConnectTimeoutMs);//发送消息给状态机的SmHandler-设置超时,延时发送和当前状态信息broadcastStateTransitions();//发送广播给BtHleper.java,该方法稍后分析}@Overridepublic boolean processMessage(Message message) {/// M: add log. @{stateLogD("processMessage:" + message.what);///// @}switch (message.what) {//该阶段的msg.what == 3,也就对应case CONNECT_AUDIOcase CONNECT:case DISCONNECT:case CONNECT_AUDIO:case DISCONNECT_AUDIO:deferMessage(message);//把msg放进消息队列break;case CONNECT_TIMEOUT: {//超时处理,我调试时没走该段代码BluetoothDevice device = (BluetoothDevice) message.obj;if (!mDevice.equals(device)) {stateLogW("CONNECT_TIMEOUT for unknown device " + device);break;}stateLogW("CONNECT_TIMEOUT");transitionTo(mConnected);break;}default: //当没有匹配的case时return super.processMessage(message);}return HANDLED;}@Overridepublic void processAudioEvent(int state) {switch (state) {case HeadsetHalConstants.AUDIO_STATE_DISCONNECTED:stateLogW("processAudioEvent: audio connection failed");transitionTo(mConnected);break;case HeadsetHalConstants.AUDIO_STATE_CONNECTING://目前匹配的该条case;常量值:1// ignore, already in audio connecting statebreak;case HeadsetHalConstants.AUDIO_STATE_DISCONNECTING:// ignore, there is no BluetoothHeadset.STATE_AUDIO_DISCONNECTINGbreak;case HeadsetHalConstants.AUDIO_STATE_CONNECTED://如果case走这一步就一切正常,能进行状态切换;常量值:2stateLogI("processAudioEvent: audio connected");transitionTo(mAudioOn);break;default:stateLogE("processAudioEvent: bad state: " + state);break;}}@Overridepublic void exit() {removeMessages(CONNECT_TIMEOUT);super.exit();}}

进入该阶段时,调用processMessage方法,会调用N次,第一次为msg.what == 3是正常的,看log日志后发现还有101的值传过来,该方法里匹配不到则走了default,导致循环调用,最后走到了processAudioEvent,state为1,产生的异常,那么这个101的值代表什么呢?又从哪里进来的?带着这个疑问继续跟踪代码

static final int STACK_EVENT = 101; //看该常量的意思,应该是发送粘性事件

发现它在HeadsetService.java中被调用了,跟着走,上代码

 /*** Handle messages from native (JNI) to Java. This needs to be synchronized to avoid posting* messages to state machine before start() is done** @param stackEvent event from native stack*/void messageFromNative(HeadsetStackEvent stackEvent) {Objects.requireNonNull(stackEvent.device,"Device should never be null, event: " + stackEvent);synchronized (mStateMachines) {HeadsetStateMachine stateMachine = mStateMachines.get(stackEvent.device);if (stackEvent.type == HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) {switch (stackEvent.valueInt) {case HeadsetHalConstants.CONNECTION_STATE_CONNECTED:case HeadsetHalConstants.CONNECTION_STATE_CONNECTING: {// Create new state machine if none is foundif (stateMachine == null) {stateMachine = HeadsetObjectsFactory.getInstance().makeStateMachine(stackEvent.device,mStateMachinesThread.getLooper(), this, mAdapterService,mNativeInterface, mSystemInterface);mStateMachines.put(stackEvent.device, stateMachine);}break;}}}if (stateMachine == null) {throw new IllegalStateException("State machine not found for stack event: " + stackEvent);}//101值在这里进行调用了,发送到AudioConnecting阶段stateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT, stackEvent);}}

注意看最开始的官方注释,这是一套JNI代码,说明这里肯定是走了底层;我又有两个疑惑了

  1. 走AudioConnecting阶段时,正常的话是不是不会走messageFromNative方法
  2. 如果正常的情况也要走该段代码,这个101值是必传给AudioConnecting的,那么是不是传过去后这个101值处理有问题

带着这两个问题,又继续出发,看看messageFromNative是在哪里被调用的,发现在HeadsetNativeInterface.java#sendMessageToService中去调用了,而且不止一处被调用,很多JNI相关的方法都执行了该方法,具体来看看一段截图
在这里插入图片描述以上这些方法都是底层回调数据到上层的方法,这下刚刚的疑惑就得到解决了,那么这个101状态值肯定是必须要走的,期间把event作为传入传递过去了,其中event就包含了state和type信息,记住此时的state为1,type为EVENT_TYPE_AUDIO_STATE_CHANGED,至此源头就找到了,接着回到AudioConnecting连接状态,看如下片段代码

     @Overridepublic boolean processMessage(Message message) {/// M: add log. @{stateLogD("processMessage:" + message.what);// 此时这里的what已经是101了/// @}switch (message.what) {case CONNECT:case DISCONNECT:case CONNECT_AUDIO:case DISCONNECT_AUDIO:deferMessage(message);break;case CONNECT_TIMEOUT: {BluetoothDevice device = (BluetoothDevice) message.obj;if (!mDevice.equals(device)) {stateLogW("CONNECT_TIMEOUT for unknown device " + device);break;}stateLogW("CONNECT_TIMEOUT");transitionTo(mConnected);break;}default: //因为没有101常量值匹配,走defaultreturn super.processMessage(message);}return HANDLED;}

再看看super.processMessage(msg);上代码

 /*** Handle common messages in connected states. However, state specific messages must be* handled individually.** @param message Incoming message to handle* @return True if handled successfully, False otherwise*/@Overridepublic boolean processMessage(Message message) {switch (message.what) {... case STACK_EVENT: //此时走了该case,因为子类把message也传过来了HeadsetStackEvent event = (HeadsetStackEvent) message.obj;stateLogD("STACK_EVENT: " + event);if (!mDevice.equals(event.device)) {stateLogE("Event device does not match currentDevice[" + mDevice+ "], event: " + event);break;}switch (event.type) { //拿到底层给的event数据case HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:processConnectionEvent(message, event.valueInt);break;case HeadsetStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED://正好type值匹配的该条caseprocessAudioEvent(event.valueInt); //该方法是AudioConnecting连接阶段的一个回调方法,而event.valueInt == 1,也就是state == 1break;...}break;default:stateLogE("Unexpected msg " + getMessageName(message.what) + ": " + message);return NOT_HANDLED;}return HANDLED;}

这时回到了AudioConnecting连接状态的processAudioEvent方法,上面已经提到过了,再来看一遍

@Overridepublic void processAudioEvent(int state) {//此时为1switch (state) {case HeadsetHalConstants.AUDIO_STATE_DISCONNECTED:stateLogW("processAudioEvent: audio connection failed");transitionTo(mConnected);break;case HeadsetHalConstants.AUDIO_STATE_CONNECTING://该常量值:1// ignore, already in audio connecting statebreak;case HeadsetHalConstants.AUDIO_STATE_DISCONNECTING:// ignore, there is no BluetoothHeadset.STATE_AUDIO_DISCONNECTINGbreak;case HeadsetHalConstants.AUDIO_STATE_CONNECTED://该常量值:2stateLogI("processAudioEvent: audio connected");transitionTo(mAudioOn);break;default:stateLogE("processAudioEvent: bad state: " + state);break;}}

而刚好state为1,走的 case HeadsetHalConstants.AUDIO_STATE_CONNECTING,导致直接break;什么事情都没做,如果此时state为2,则进入case HeadsetHalConstants.AUDIO_STATE_CONNECTED,再调用transitionTo(mAudioOn);进行状态切换,就一切正常了,那么基于之前的一些疑问,在这里就已经解决了

分析完上层,接着看底层是怎么把state值回调到上层的。

3.4 底层状态分析

com_android_bluetooth_hfp.cpp

void AudioStateCallback(bluetooth::headset::bthf_audio_state_t state,RawAddress* bd_addr) override {ALOGI("%s, %d for %s", __func__, state, bd_addr->ToString().c_str());std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);CallbackEnv sCallbackEnv(__func__);if (!sCallbackEnv.valid() || !mCallbacksObj) return;ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));if (!addr.get()) return;// (jint)state 该值将回调到上层sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onAudioStateChanged,(jint)state, addr.get());}

接着跟踪state值怎么拿到的

btif_hf.cc

bt_status_t HeadsetInterface::ConnectAudio(RawAddress* bd_addr) {CHECK_BTHF_INIT();int idx = btif_hf_idx_by_bdaddr(bd_addr);if ((idx < 0) || (idx >= BTA_AG_MAX_NUM_CLIENTS)) {BTIF_TRACE_ERROR("%s: Invalid index %d", __func__, idx);return BT_STATUS_FAIL;}/* Check if SLC is connected */if (!IsSlcConnected(bd_addr)) {LOG(ERROR) << ": SLC not connected for " << *bd_addr;return BT_STATUS_NOT_READY;}// 注意看do_in_jni_thread(base::Bind(&Callbacks::AudioStateCallback,// Manual pointer management for nowbase::Unretained(bt_hf_callbacks),BTHF_AUDIO_STATE_CONNECTING,//该值就是state,而恰好这个常量就是1,上层也是卡在connecting无法执行&btif_hf_cb[idx].connected_bda));BTA_AgAudioOpen(btif_hf_cb[idx].handle);return BT_STATUS_SUCCESS;
}

BTHF_AUDIO_STATE_CONNECTING该值就是state =1 ,说明还没执行完就在这卡住了,所以上层也会卡在connecting。如果继续执行的话 --看代码

static void btif_hf_upstreams_evt(uint16_t event, char* p_param) {...case BTA_AG_WBS_EVT: //先走这条case,用于编解码音频BTIF_TRACE_DEBUG("BTA_AG_WBS_EVT Set codec status %d codec %d 1=CVSD 2=MSBC",p_data->val.hdr.status, p_data->val.num);//如果p_data->val.num = 1则执行CVSD编码,为2的话走MSBC编码if (p_data->val.num == BTA_AG_CODEC_CVSD) {bt_hf_callbacks->WbsCallback(BTHF_WBS_NO,&btif_hf_cb[idx].connected_bda);} else if (p_data->val.num == BTA_AG_CODEC_MSBC) {bt_hf_callbacks->WbsCallback(BTHF_WBS_YES,&btif_hf_cb[idx].connected_bda);} else {bt_hf_callbacks->WbsCallback(BTHF_WBS_NONE,&btif_hf_cb[idx].connected_bda);}break;...case BTA_AG_AUDIO_OPEN_EVT://然后走这条case,把state=2回调给上层bt_hf_callbacks->AudioStateCallback(BTHF_AUDIO_STATE_CONNECTED,&btif_hf_cb[idx].connected_bda);break;...
}

btif_hf_upstreams_evt方法会被多次执行,具体怎么执行的暂不清楚,上面很关键的两条case,选择音频编解码和state=2回调到上层,但是我遇到的问题就是只走了编解码,state=2的case没有执行

至此 整个流程差不多就要结束了,回到上层来,既然状态机的各个阶段都正常的话,是不是该把广播发出去,通知各个方法。接着看到broadcastStateTransitions()方法,它在每个状态机阶段都会执行一次,发送不同阶段的广播以便于知道执行到哪个阶段了

void broadcastStateTransitions() {if (mPrevState == null) {return;}// TODO: Add STATE_AUDIO_DISCONNECTING constant to get rid of the 2nd part of this logicif (getAudioStateInt() != mPrevState.getAudioStateInt() || (mPrevState instanceof AudioDisconnecting && this instanceof AudioOn)) {stateLogD("audio state changed: " + mDevice + ": " + mPrevState + " -> " + this);broadcastAudioState(mDevice, mPrevState.getAudioStateInt(), getAudioStateInt());//发送当前的device和上一个状态和当前状态,这个状态指的是每个状态机的 getAudioStateInt 方法返回的值 ,AudioConnecting状态机的返回值=11,AudioOn状态机的返回值=12}if (getConnectionStateInt() != mPrevState.getConnectionStateInt()) {stateLogD("connection state changed: " + mDevice + ": " + mPrevState + " -> " + this);broadcastConnectionState(mDevice, mPrevState.getConnectionStateInt(),getConnectionStateInt());}}//发送广播void broadcastAudioState(BluetoothDevice device, int fromState, int toState) {stateLogD("broadcastAudioState: " + device + ": " + fromState + "->" + toState);StatsLog.write(StatsLog.BLUETOOTH_SCO_CONNECTION_STATE_CHANGED,mAdapterService.obfuscateAddress(device),getConnectionStateFromAudioState(toState),TextUtils.equals(mAudioParams.get(HEADSET_WBS), HEADSET_AUDIO_FEATURE_ON)? BluetoothHfpProtoEnums.SCO_CODEC_MSBC: BluetoothHfpProtoEnums.SCO_CODEC_CVSD);mHeadsetService.onAudioStateChangedFromStateMachine(device, fromState, toState);Intent intent = new Intent(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);//写入当前state状态intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, fromState);intent.putExtra(BluetoothProfile.EXTRA_STATE, toState);intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);mHeadsetService.sendBroadcastAsUser(intent, UserHandle.ALL,HeadsetService.BLUETOOTH_PERM);}

该广播会在BtHelper.java#receiveBtEvent中接收

    @GuardedBy("AudioDeviceBroker.mDeviceStateLock")/*package*/ synchronized void receiveBtEvent(Intent intent) {final String action = intent.getAction();if (action.equals(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)) {...} else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {boolean broadcast = false;int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR;int btState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);//拿到state值// broadcast intent if the connection was initated by AudioService...switch (btState) {// 正常的话 state状态值为12case BluetoothHeadset.STATE_AUDIO_CONNECTED://该值=12scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED;if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL&& mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;}//开启SCO服务,再执行startBluetoothScoOnmDeviceBroker.setBluetoothScoOn(true, "BtHelper.receiveBtEvent");break;...case BluetoothHeadset.STATE_AUDIO_CONNECTING://该值=11if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL&& mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;}break;default:// do not broadcast CONNECTING or invalid statebroadcast = false;break;}//当state=12时,broadcast=true,代码没贴进来--作用是建立SCO服务if (broadcast) {broadcastScoConnectionState(scoAudioState);...Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED);newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, scoAudioState);sendStickyBroadcastToAll(newIntent);}}}

至此,全部流程结束

四.蓝牙通话无声问题解决

[根本原因]

state卡在了BTHF_AUDIO_STATE_CONNECTING,执行了音频编解码后没继续往后执行,根据log提示,SCO link无法响应,无法连接,导致SCO服务挂掉 进而产生的一些列异常;

[解决办法]

既然是SCO link无法响应,那就是Modem端无法给出正确的response;发现是蓝牙端本身的一个bug,需要合入MTK提供的俩个official patch

合入过后观察底层和上层的state值和状态机,发现一切正常,但是…还是通话无声…问题又来了…原因是要 enable merge interface ,那就来开启该宏

[音频无声解决办法]

1.ProjectConfig.mk # MTK_MERGE_INTERFACE_SUPPORT=yes

2.kernel-4.9/arch/arm64/configs/dl35_debug_defconfig # CONFIG_MTK_MERGE_INTERFACE_SUPPORT=y

3.kernel-4.9/arch/arm64/configs/dl35_defconfig # CONFIG_MTK_MERGE_INTERFACE_SUPPORT=y

至此 问题解决


http://chatgpt.dhexx.cn/article/1GWqR5il.shtml

相关文章

BQB pts测试

测试BQB的pts dongle是在蓝牙SIG官网买的,链接是 https://store.bluetooth.com/12210888/orders/d3b63cfd9d3d5a22c2e08ad9711a91c4 pts dongle最新的测试结果 ACS-BV-07-I/ACS-BV-12-I/ACS-BI-13-I 这三个是接完电话就fail ICA-BV-02-I/TCA-BV-01-I 这两个是挂不了电话 ICR-B…

蓝牙btsnoop log,HFP协议连接流程详解,以及RFCOMM连接和常用AT指令

HFP&#xff08;Hands-Free&#xff09;&#xff1a;蓝牙免提协议,两个角色AG&#xff0c;HF端&#xff0c;AG端通常是手机设备&#xff0c;HF免提端一般为耳机车载等&#xff0c;hfp的连接首先要进行SDP&#xff0c;然后建立RFCOMM&#xff0c;然后SLC连接完成&#xff0c;HFP…

蓝牙协议HFP(Hands-Free Profile)电话免提协议 Connection management 连接管理HFP SLC 的建立跟释放

零. 概述 本文章主要讲下电话免提协议HFP&#xff08;Hands-Free Profile&#xff09;Connection management。包括connection establishment 跟connection realease&#xff0c;那connection establishment又会涉及到HFP SLC的建立过程。 本节讲解的内容就是一下HFP fea…

蓝牙A2DP和HFP编解码

一、A2DP A2DP全名是&#xff08;Advanced Audio Distribution Profile&#xff09; 蓝牙音频传输模型协定&#xff0c;提供通过蓝牙连接传输音频流的能力&#xff0c;比如手机播放音乐&#xff0c;蓝牙耳机通过蓝牙连接听歌。 mp3和flac音频编码都是在PCM音频编码基础上二次编…

Rockchip安卓11.0 16k wbs msbc HFP PCM语音通话支持

Rockchip安卓11.0 16k wbs/msbc HFP PCM语音通话支持 调试平台: 安卓11.0, rk3328, 博通ap6212芯片, HFP 8K已经调通的情况下. SDK修改支持16k wbs/msbc HFP PCM语音注意点如下: 1. bluedroid(system/bt) 博通方案中, ESCO_DATA_PATH_PCM 为1代表蓝牙芯片作为pcm master, 6…

蓝牙电话之HFP-电话音频

蓝牙电话之HFP协议中的电话音频 蓝牙技术通信的内容多种多样&#xff0c;其中音频部分包含媒体音频和电话音频。 媒体音频&#xff1a;播放蓝牙音乐的数据&#xff0c;这种音频对质量要求高&#xff0c;数据发送有重传机制&#xff0c;从而以l2cap的数据形式走ACL链路。编码方…

VSCode 按下tab键自动补全功能失效

问题描述&#xff1a;标签自动补全插件已经安装&#xff0c;某天按下tab键自动补全标签的功能突然失效了。解决方法&#xff1a; 1、找到路径 file > Preferences >settings 2、搜索tabs&#xff0c;选择Extensions里面的Emmet&#xff0c;将复选框的√打上就ok啦!

javascript 按下回车键触发input表单的切换(enter键代替tab键功能)

本来是想用js代码去模拟键盘按下来实现该功能&#xff0c;但是没有实现到。后来想到一种比较笨的方法。局限性是只能这些表单元素只能在同个层面下才能切换。 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><m…

sublime text3 tab键功能失效

选择首选项 -> 快捷键设置 出现下面编辑页 在右边添加下面一些设置&#xff08;记得要用逗号与前面内容分开&#xff09;&#xff1a; { "keys": ["tab"], "command": "reindent", "context":[{ "key": "…

计算机键盘上的2个定位键,电脑键盘上Tab 键的神奇之处!(用途详解)

下面通过实例来详细解释下&#xff1a; 假设“D:\cfan”路径下&#xff0c;有文件夹&#xff1a; a、 ab &#xff0c;有文件&#xff1a; abc.txt &#xff0c; ac.txt 。依次点击“开始→运行”输入“cmd”&#xff0c;按下回车打开命令提示符。先输入“D:”&#xff0c;回车…

微型计算机上的tab作用,TAB键有什么用处

Tab键对大家来说都是比较熟悉吧。比如写文章时开头要空两格就可用这个键。但是它还有什么特殊功能呢&#xff0c;不一定是大家都知道的。下面小编来详细的介绍下几种tab键的常见功能 tab键作用一&#xff1a;改变焦点 聚焦到下一个按钮&#xff0c;输入框或者链接等。例如&…

计算机键盘上的tab键是什么键,电脑Tab键有什么用处

电脑Tab键有什么用处 Tab键对大家来说都是比较熟悉吧。比如写文章时开头要空两格就可用这个键。但是它还有什么特殊功能呢&#xff0c;不一定是大家都知道的。下面小编来详细的介绍下几种tab键的常见用处&#xff1a; tab键作用一&#xff1a;改变焦点 聚焦到下一个按钮&#x…

电脑Tab键有什么功能?6个实用功能总结!

我最近对电脑的键盘很有兴趣&#xff0c;想了解一下各个键有什么特殊的用法。今天正好看到Tab键&#xff0c;想问下大家知道电脑的Tab键有什么比较好用的功能吗&#xff1f; 在电脑键盘上&#xff0c;Tab键是一个常见的键&#xff0c;它具有多种功能和用途。无论是在文本处理、…

tab键功能

tab 键可以实现命令及路径等补全&#xff0c;提高输入效率&#xff0c;避免出错 tab键可以命令补全&#xff0c;还有路径、文件名补全。 命令补全&#xff1a; 在Linux命令行下&#xff0c;输入字符后&#xff0c;按两次Tab键&#xff0c;shell就会列出以这些字符打头的所有…

计算机键盘上的tab键是什么键,键盘Tab键有什么作用?

Tab键——跳格键&#xff0c;大家都比较熟悉了吧&#xff0c;但是到底它有什么特殊的使用功能&#xff0c;却未必是大家都知道的哦。今天就将Tab键的几种使用功能进行了整理&#xff0c;大家可以参考学习学习。 1、跳格。当我们在网页输入表单的时候&#xff0c;在一个输入控件…

电脑Tab键有什么用

Tab键对大家来说都是比较熟悉吧。比如写文章时开头要空两格就可用这个键。但是它还有什么特殊功能呢&#xff0c;不一定是大家都知道的。下面我来详细的介绍下几种tab键的常见用处&#xff1a; tab键作用一&#xff1a;改变焦点 聚焦到下一个按钮&#xff0c;输入框或者链接等…

电脑Tab键有什么功能?分享Tab键的6个妙用

Tab键tabulator key 的缩写&#xff0c;意思是跳格键。基本用法是可以用来绘制无边框的表格&#xff0c;还可以在单词间留下间隔&#xff0c;一般等于八个空格的长度。但是您知道电脑Tab键有什么功能吗&#xff1f;以下一些关于Tab键的使用和功能的介绍&#xff0c;希望这些可以…

matlab EOF程序

% eof第一模态图用变量eof的第一列&#xff0c;以此类推&#xff1b;相应的时间系数用pc第一行 clear clcx [2 6 1 5 2;9 4 0 5 4;12 2 55 9 10;4 55 78 2 13]; %原始数据&#xff0c;列为站点或格点&#xff0c;行为时间序列上的值 x(1,:) x(1,:) - mean(x(1,:)); x(…

C语言——EOF的用法——while(scanf(“%d“,num)!=EOF)

简介&#xff1a; EOF是一个计算机术语&#xff0c;为End Of File的缩写&#xff0c;在操作系统中表示资料源无更多的资料可读取。资料源通常称为档案或串流。通常在文本的最后存在此字符表示资料结束。、 概念&#xff1a; 在C语言中&#xff0c;或更精确地说成C标准函数库中…

cat << EOF 什么意思?

cat <<EOF 什么意思&#xff1f; cat命令表示查看&#xff0c;而cat <<EOF命令表示将进行输入&#xff0c;直到以EOF终止符来结束输入&#xff08;最后的新行&#xff09;。EOF必须写在一行的头部&#xff0c;前面不能有制表符或者空格。如果结束符EOF前面有制表符…