【蓝牙开发】Andorid蓝牙绝对音量

2023-10-26

蓝牙绝对音量相关基础知识
1. 什么是绝对音量

Android 蓝牙部分的官方文档有如下描述:
Androud Bluetooth Service

在 Android 6.0 及更高版本中,Android 蓝牙堆栈允许音频源设置绝对音量,以便用户准确控制音频音量。音频源设备会将音量信息和未衰减的音频发送到接收器。然后,接收器会根据音量信息放大音频,以便用户听到准确的播放音量。
音频源设备还可以注册接收音量通知。进行此项注册后,当用户使用接收器上的控件更改音量时,接收器便会向音频源发送通知。这样一来,音频源便能够准确地在界面上显示音量信息。
绝对音量控制默认处于开启状态。如需停用绝对音量控制,用户可以依次转到设置 > 系统 > 开发者选项,然后选择停用绝对音量功能开关。

而蓝牙绝对音量(absolute volume)的概念是在AVRCP Sepc 1.4中引入的, 是为了替代蓝牙中的相对音量(relative volume)命令: volume up 和 volume down, 之前的相对音量是不考虑对方的Target device是处于最大音量或在最小音量.

2. AVRCP中的Controller和Target 两种角色的理解

在这里插入图片描述
首先看下两种角色在协议构成角度来说是没有太大的区别, 现在来看下AVRCP Spec中关于AVRCP中Controller(CT)和Target(TG)的描述.

  • The controller (CT) is a device that initiates a transaction by sending a command frame to a target. Examples for CT are a personal computer, a PDA, a mobile phone, a remote controller or an AV device (such as an in car system, headphone, player/recorder, timer, tuner, monitor etc.).
    将发起AVRC控制命令到对方的蓝牙设备称为CT

  • The target (TG) is a device that receives a command frame and accordingly generates a response frame. Examples for TG are an audio player/recorder, a video player/recorder, a TV, a tuner, an amplifier or a headphone.
    将接收AVRC控制命令并回复respond的蓝牙设备称为TG

例如手机和车载蓝牙为例子:
在这里插入图片描述
如上图所示, 是车载蓝牙发起控制命令, 所以在这里车载蓝牙是CT,而手机是TG.

补充说明:
针对AVRCP中的CT和TG的角色定义 和谁发起蓝牙连接是无关的.
另外一个蓝牙蓝牙设备是可以同时支持CT和TG两种角色的(这点在后续讲到A2DP Source和Sink的时候会在详细讲解).

3. 蓝牙绝对音量(Absolute)的定义

蓝牙绝对音量的特性提供了音量处理的功能能够允许CT进行音量变化的展示.
在AVRCP的Spec中定义了两个命令用于绝对音量的设置以及音量变化的监听.

SetAbsoluteVolume命令用于设置绝对音量
命令格式如下:
在这里插入图片描述在这里插入图片描述这里以手机连接蓝牙音箱为例子, 在这个场景下手机为AVRCP CT,蓝牙音箱为AVRCP TG.
当在手机端进行音量调节时,手机端会发送SetAbsoluteVolume命令到音箱端进行音量的调节,
需要注意的是,当手机开启绝对音量的功能后,在手机端调节音量并不会对传输的音源进行音量处理而是通过TG进行音量的调节.

Notify Volume Change
RegisterNotification(Volume Changed Event)命令用于CT进行TG上本地音量的监听以及对于TG的音量同步.
命令格式定义如下:
在这里插入图片描述
下面可以看下AVRCP Spec中关于它的描述:
在这里插入图片描述
在CT和TG建立连接的时候,CT会注册RegisterNotification(Volume Changed)到TG, TG在接收到该注册请求后,会先返回一个临时的response(仅是获取当前TG的音量同步到CT). 之后只有在用户操作TG的本地音量后会再次同步一个RegisterNotification response(同步当前的TG音量). 而这才是完成一次Volume Change Notification.

这里需要注意的是, RegisterNotification(Volume Changed)的流程是不会因为CT发起了SetAbsoluteVolume命令打断的.

4. 什么情况下蓝牙设备需要支持绝对音量呢

AVRCP V1.4及以上版本SDP中AVRCP 的分类:Category 2需支持绝对音量
而GetCapabilities中的EVENT_VOLUME_CHANGED 标志会影响CT端是否会发起SetAbsoluteVolume.
关于绝对音量的支持,由于Spec中没有特明确的规定导致不同厂商在实现上会有些差异,也就导致了我们常说的兼容性问题,所以在我们掌握了什么是绝对音量后可以根据实际抓到数据进行特定适配.


Android 蓝牙中绝对音量的实现

本文是基于一款基于Android系统开发的带屏音箱进行讲述, 该音箱实现了A2DP Sink 和A2DP Source.
而A2DP Sink是和AVRCP Target搭配,A2DP Source是和AVRCP Controller搭配.

手机连接音箱场景下音箱端绝对音量的实现

手机连接音箱进行蓝牙音乐播放场景下,手机是: A2DP Source + AVRCP Controller
而音箱端是: A2DP Sink + AVRCP Target

1. 基于Bluetooth Spec 讲述绝对音量的控制流程

手机端在与音箱端建立蓝牙连接后,发出"Get Capabilities PDU 0x10", 问询音箱端支持的event
音箱端在接收到"Get Capabilities PDU"后,回复所支持的event, 其中包含: (absolute)volume changed - 0x0d

手机端在收到response后,看到音箱端支持"(absolute)volume changed", 于是发起了notification register - “registers for volume changed notifications”

然后音箱端需要立即回复一个临时的response(interim response),将当前音箱端(Target)的音量同步到手机(Controller)

手机端在收到该response后就可以及时的通过音量条显示,而手机端希望调大音量 会发出"SET_ABSOLUTE_VOLUME"命令到音箱端进行音量的设置

音箱端在接收到该命令后,会进行音量的设置 并返回真正设置的音量到手机端, 而手机端会基于该返回的音量值进行显示

而此刻当用户在音箱端主动进行音量的调节后,会触发"Voloume Changed notification" 通知到手机端.
而手机端希望再次监听音箱端的音量变化需要重新注册notification register - “registers for volume changed notifications”.
在这里插入图片描述

2. 基于音箱端抓取到HCI日志分析绝对音量控制流程

待输出

3. 音箱端(Android: A2DP Sink + AVRCP Target)的绝对音量的相关代码分析

补充说明的是,关于Android系统下蓝牙的相关基础知识会在其它文章中进行分享,本文是基于大家对于Android 系统蓝牙有了基本了解的前提下展开.
下面来分析下当手机端发起notification register - “registers for volume changed notifications” 和 SET_ABSOLUTE_VOLUME指令过程中音箱端相关代码的实现.

在system/bt(bluedroid)中主要是对手机发过来的registers for volume changed notifications 进行数据的解析.

// system/bt/btif/src/btif_rc.cc
void btif_rc_handler(tBTA_AV_EVT event, tBTA_AV* p_data) {
...
    case BTA_AV_META_MSG_EVT: {
      ...
      } else if (bt_rc_ctrl_callbacks != NULL) {
        /* This is case of Sink + CT + TG(for abs vol)) */
        switch (p_data->meta_msg.p_msg->hdr.opcode) {
          case AVRC_OP_VENDOR:
            if ((p_data->meta_msg.code >= AVRC_RSP_NOT_IMPL) &&
                (p_data->meta_msg.code <= AVRC_RSP_INTERIM)) {
              /* Its a response */
              handle_avk_rc_metamsg_rsp(&(p_data->meta_msg));
            } else if (p_data->meta_msg.code <= AVRC_CMD_GEN_INQ) {
              /* Its a command  */
              handle_avk_rc_metamsg_cmd(&(p_data->meta_msg));
            }
            break;    
}

// system/bt/btif/src/btif_rc.cc
// handle_avk_rc_metamsg_cmd
static void handle_avk_rc_metamsg_cmd(tBTA_AV_META_MSG* pmeta_msg) {
    ...
      if (avrc_cmd.pdu == AVRC_PDU_REGISTER_NOTIFICATION) {
        uint8_t event_id = avrc_cmd.reg_notif.event_id;
        BTIF_TRACE_EVENT("%s: Register notification event_id: %s", __func__,
                         dump_rc_notification_event_id(event_id));
      } else if (avrc_cmd.pdu == AVRC_PDU_SET_ABSOLUTE_VOLUME) {
        BTIF_TRACE_EVENT("%s: Abs Volume Cmd Recvd", __func__);
      }

      btif_rc_ctrl_upstreams_rsp_cmd(avrc_cmd.pdu, &avrc_cmd, pmeta_msg->label,
                                     p_dev);
                                     
 }
 
 // 在btif_rc_ctrl_upstreams_rsp_cmd回调Bluetoth Process中往Bluedorid中注册的registernotification_absvol_cb
 static void btif_rc_ctrl_upstreams_rsp_cmd(uint8_t event,
                                           tAVRC_COMMAND* pavrc_cmd,
                                           uint8_t label,
                                           btif_rc_device_cb_t* p_dev) {
  BTIF_TRACE_DEBUG("%s: pdu: %s: handle: 0x%x", __func__,
                   dump_rc_pdu(pavrc_cmd->pdu), p_dev->rc_handle);
  switch (event) {
    case AVRC_PDU_REGISTER_NOTIFICATION:
      if (pavrc_cmd->reg_notif.event_id == AVRC_EVT_VOLUME_CHANGE) {
        // 最终会回调Bluetooth/jni/com_android_bluetooth_avrcp_controller.cpp 中定义的
        do_in_jni_thread(
            FROM_HERE,
            base::Bind(bt_rc_ctrl_callbacks->registernotification_absvol_cb,
                       p_dev->rc_addr, label));
      }
      break;
  }
}

在system/bt(Bluedorid)处理解析"registers for volume changed notifications“ 后会回调到Bluetooth App中的相关代码, 并通过Bluetooth App中获取到当前系统音量后,返回interim response到手机端.

// /packages/apps/Bluetooth/jni/com_android_bluetooth_avrcp_controller.cpp

static void btavrcp_register_notification_absvol_callback(
    const RawAddress& bd_addr, uint8_t label) {
  ALOGI("%s", __func__);
  CallbackEnv sCallbackEnv(__func__);
  if (!sCallbackEnv.valid()) return;

  ScopedLocalRef<jbyteArray> addr(
      sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
  if (!addr.get()) {
    ALOGE("Fail to get new array ");
    return;
  }

  sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
                                   (jbyte*)&bd_addr.address);
  // jni 方式回调Java的API method_handleRegisterNotificationAbsVol - handleRegisterNotificationAbsVol
  sCallbackEnv->CallVoidMethod(sCallbacksObj,
                               method_handleRegisterNotificationAbsVol,
                               addr.get(), (jbyte)label);
}

// /packages/apps/Bluetooth/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java

// Called by JNI when remote wants to receive absolute volume notifications.
    private synchronized void handleRegisterNotificationAbsVol(byte[] address, byte label) {
        Log.d(TAG, "handleRegisterNotificationAbsVol ");
        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
        if (device != null && !device.equals(mConnectedDevice)) {
            Log.e(TAG, "handleRegisterNotificationAbsVol device not found " + address);
            return;
        }
        Message msg = mAvrcpCtSm.obtainMessage(
                AvrcpControllerStateMachine.MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION,
                (int) label, 0);
        mAvrcpCtSm.sendMessage(msg);
    }

// 发送MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION到ArcpController的状态机中进一步处理
// /packages/apps/Bluetooth/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
// Connected状态下进行处理
 class Connected extends State {
     public boolean processMessage(Message msg) {
     A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();
            synchronized (mLock) {
                switch (msg.what) {
                case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION: {
                        mRemoteDevice.setNotificationLabel(msg.arg1);
                        mRemoteDevice.setAbsVolNotificationRequested(true);
                        int percentageVol = getVolumePercentage();
                        if (DBG) {
                            Log.d(TAG, " Sending Interim Response = " + percentageVol + " label "
                                    + msg.arg1);
                        }
                        // 通过JNI方式调用sendRegisterAbsVolRspNative
                        AvrcpControllerService.sendRegisterAbsVolRspNative(
                                mRemoteDevice.getBluetoothAddress(), NOTIFICATION_RSP_TYPE_INTERIM,
                                percentageVol, mRemoteDevice.getNotificationLabel());
                    }
                    break;
                ...
}

// /packages/apps/Bluetooth/jni/com_android_bluetooth_avrcp_controller.cpp
static void sendRegisterAbsVolRspNative(JNIEnv* env, jobject object, ...{
    // system/bt中register_abs_vol_rs(实际函数: volume_change_notification_rsp)
    bt_status_t status = sBluetoothAvrcpInterface->register_abs_vol_rsp(
      rawAddress, (btrc_notification_type_t)rsp_type, (uint8_t)abs_vol,
      (uint8_t)label);
}

// system/bt/btif/src/btif_rc.cc
/***************************************************************************
 *
 * Function         send_register_abs_vol_rsp
 *
 * Description      Rsp for Notification of Absolute Volume
 *
 * Returns          void
 *
 **************************************************************************/
static bt_status_t volume_change_notification_rsp(...
{
  // 这里就比较好理解了 数据格式在AVRCP Sepc中有详细的描述
  avrc_rsp.reg_notif.opcode = AVRC_OP_VENDOR;
  avrc_rsp.reg_notif.pdu = AVRC_PDU_REGISTER_NOTIFICATION;
  avrc_rsp.reg_notif.status = AVRC_STS_NO_ERROR;
  avrc_rsp.reg_notif.param.volume = abs_vol;
  avrc_rsp.reg_notif.event_id = AVRC_EVT_VOLUME_CHANGE;

  status = AVRC_BldResponse(p_dev->rc_handle, &avrc_rsp, &p_msg);
  if (status == AVRC_STS_NO_ERROR) {
    BTIF_TRACE_DEBUG("%s: msgreq being sent out with label: %d", __func__,
                     label);
    uint8_t* data_start = (uint8_t*)(p_msg + 1) + p_msg->offset;
    BTA_AvVendorRsp(p_dev->rc_handle, label,
                    (rsp_type == BTRC_NOTIFICATION_TYPE_INTERIM)
                        ? AVRC_RSP_INTERIM
                        : AVRC_RSP_CHANGED,
                    data_start, p_msg->len, 0);
}

另外这里将上述的代码调用过程整理成了下面的UML图,由于图片较大 需要令打开一个窗口进行放大查看.
在这里插入图片描述
以上讲述了手机向音箱注册“volume changed notifications“时 音箱端的处理流程,而在音箱接受这个注册后再进行音箱端的音量调节时会有哪些行为呢?请看接下来的分析

// /packages/apps/Bluetooth/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
// BroadcastReceiver 监听系统音量的变化,在用户进行音量调节时, 这里监听到后会发送MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) {
                int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
                if (streamType == AudioManager.STREAM_MUSIC) {
                    sendMessage(MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION);
                }
            }
        }
    };
  
// 
class Connected extends State {
        @Override
        public boolean processMessage(Message msg) {
            if (DBG) Log.d(TAG, " HandleMessage: " + dumpMessageString(msg.what));
            A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();
            synchronized (mLock) {
                switch (msg.what) {
                 case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION: {

                     if (mRemoteDevice.getAbsVolNotificationRequested()) {
                         int percentageVol = getVolumePercentage();
                         if (percentageVol != mPreviousPercentageVol) {
                             // 通过JNI 方式 sendRegisterAbsVolRspNative
                             // 最终会通过system/bt中的volume_change_notification_rsp
                             // 这里大家会注意 这个接口不就是前面提到的Interim Response 是一样的吗? 是一样 这是由于这两个动作的数据格式和行为本质上是相同的
                             // 一个是当手机发起注册行为时需要立即回复
                             // 一个是在用户调节音量后进行回复
                             AvrcpControllerService.sendRegisterAbsVolRspNative(mRemoteDevice.getBluetoothAddress(), NOTIFICATION_RSP_TYPE_CHANGED, percentageVol,mRemoteDevice.getNotificationLabel());
                          }
                     }
              }
              
              ...
}

补充说明:
需要注意的是, 用户在调节音量并回复response到手机后,手机如果期望继续监听音箱端音量的变化 需要再次注册"volume changed notifications"

下面继续看下当手机端发出“SET_ABSOLUTE_VOLUME”命令时,音箱端的处理流程.

在system/bt中SET_ABSOLUTE_VOLUME的处理入口和“volume changed notifications”一样, 是在
handle_avk_rc_metamsg_cmd.

// system/bt/btif/src/btif_rc.cc
static void handle_avk_rc_metamsg_cmd(tBTA_AV_META_MSG* pmeta_msg) {
    ...
     if (avrc_cmd.pdu == AVRC_PDU_REGISTER_NOTIFICATION) {
        uint8_t event_id = avrc_cmd.reg_notif.event_id;
        BTIF_TRACE_EVENT("%s: Register notification event_id: %s", __func__,
                         dump_rc_notification_event_id(event_id));
      } else if (avrc_cmd.pdu == AVRC_PDU_SET_ABSOLUTE_VOLUME) {
        BTIF_TRACE_EVENT("%s: Abs Volume Cmd Recvd", __func__);
      }

      btif_rc_ctrl_upstreams_rsp_cmd(avrc_cmd.pdu, &avrc_cmd, pmeta_msg->label,
                                     p_dev);
   ...
}

// 在btif_rc_ctrl_upstreams_rsp_cmd中通过JNI的方式 调用Bluetooth Precess中定义的API
static void btif_rc_ctrl_upstreams_rsp_cmd(uint8_t event,
                                           tAVRC_COMMAND* pavrc_cmd,
                                           uint8_t label,
                                           btif_rc_device_cb_t* p_dev) {
  BTIF_TRACE_DEBUG("%s: pdu: %s: handle: 0x%x", __func__,
                   dump_rc_pdu(pavrc_cmd->pdu), p_dev->rc_handle);
  switch (event) {
    case AVRC_PDU_SET_ABSOLUTE_VOLUME:
      // 而这里 bt_rc_ctrl_callbacks->setabsvol_cmd_cb 这里是怎么调用的Bluetooth Process中的相关api呢?
      // 
      do_in_jni_thread(
          FROM_HERE,
          base::Bind(bt_rc_ctrl_callbacks->setabsvol_cmd_cb, p_dev->rc_addr,
                     pavrc_cmd->volume.volume, label));
      break;
    ...
  }
}

// 下来先来看下 bt_rc_ctrl_callbacks是如何被初始化的
// 通过阅读源码可以看到 bt_rc_ctrl_callbacks是在 init_ctrl中进行赋值的
static bt_status_t init_ctrl(btrc_ctrl_callbacks_t* callbacks) {
    
  if (bt_rc_ctrl_callbacks) return BT_STATUS_DONE;

  bt_rc_ctrl_callbacks = callbacks;

}

// 而init_ctrl是和bt_rc_ctrl_interface关联的也就是外部使用的初始化接口
static const btrc_ctrl_interface_t bt_rc_ctrl_interface = {
    sizeof(bt_rc_ctrl_interface),
    init_ctrl,
    ...
}

// 进一步看 在Bluetooth Proceess中是如何调用init_ctrl 进行初始化的
// /packages/apps/Bluetooth/jni/com_android_bluetooth_avrcp_controller.cpp

static void initNative(JNIEnv* env, jobject object) {
    ...
    const bt_interface_t* btInf = getBluetoothInterface();
    ...
    sBluetoothAvrcpInterface =
      (btrc_ctrl_interface_t*)btInf->get_profile_interface(
          BT_PROFILE_AV_RC_CTRL_ID);
          
    ...
    // static btrc_ctrl_callbacks_t sBluetoothAvrcpCallbacks = {
    //     btavrcp_set_abs_vol_cmd_callback
    //     ...
    // 也就是 bt_rc_ctrl_callbacks->setabsvol_cmd_cb 对应的是 btavrcp_set_abs_vol_cmd_callback
    bt_status_t status =
      sBluetoothAvrcpInterface->init(&sBluetoothAvrcpCallbacks);
}

// 继续看下 btavrcp_set_abs_vol_cmd_callback中是如何操作的.
static void btavrcp_set_abs_vol_cmd_callback(const RawAddress& bd_addr,...
{
    ...
    // method_handleSetAbsVolume --> handleSetAbsVolume
    sCallbackEnv->CallVoidMethod(sCallbacksObj, method_handleSetAbsVolume,
                               addr.get(), (jbyte)abs_vol, (jbyte)label);
}

 // Called by JNI when remote wants to set absolute volume.
    private synchronized void handleSetAbsVolume(byte[] address, byte absVol, byte label) {
        Log.d(TAG, "handleSetAbsVolume ");
        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
        if (device != null && !device.equals(mConnectedDevice)) {
            Log.e(TAG, "handleSetAbsVolume device not found " + address);
            return;
        }
        Message msg = mAvrcpCtSm.obtainMessage(
                AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_ABS_VOL_CMD, absVol, label);
        mAvrcpCtSm.sendMessage(msg);
    }

// 在ArcpController的状态机中进行处理
// /packages/apps/Bluetooth/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
class Connected extends State {
        @Override
        public boolean processMessage(Message msg) {
             case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
                        mVolumeChangedNotificationsToIgnore++;
                        removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT);
                        sendMessageDelayed(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT,
                                ABS_VOL_TIMEOUT_MILLIS);
                        setAbsVolume(msg.arg1, msg.arg2);
                        break;
                        ...
                        }
 
 

 private void setAbsVolume(int absVol, int label) {
     // 对absVol进行处理 并通过mAudioManager.setStreamVolume调整系统音量后返回到remote device
      AvrcpControllerService.sendAbsVolRspNative(mRemoteDevice.getBluetoothAddress(), absVol,
                label);
 }
 // 通过JNI的方式调用com_android_bluetooth_avrcp_controller.cpp下的sendAbsVolRspNative
 // /packages/apps/Bluetooth/jni/com_android_bluetooth_avrcp_controller.cpp
 
 static void sendAbsVolRspNative(JNIEnv* env, jobject object, jbyteArray address,...
 {
     
 }
 
 // 然后调用到system/bt下的set_volume_rsp
 // system/bt/btif/src/btif_rc.cc
 
 static bt_status_t set_volume_rsp(const RawAddress& bd_addr, uint8_t abs_vol,
                                  uint8_t label) {
                                  
     avrc_rsp.volume.opcode = AVRC_OP_VENDOR;
     avrc_rsp.volume.pdu = AVRC_PDU_SET_ABSOLUTE_VOLUME;
     avrc_rsp.volume.status = AVRC_STS_NO_ERROR;
     avrc_rsp.volume.volume = abs_vol;
     // 基于Spec中规定的AVRC_PDU_SET_ABSOLUTE_VOLUME RSP格式 回复message
     status = AVRC_BldResponse(p_dev->rc_handle, &avrc_rsp, &p_msg);
     BTA_AvVendorRsp(p_dev->rc_handle, label, AVRC_RSP_ACCEPT, data_start,
                      p_msg->len, 0);
                      ...
}                             
                                  

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【蓝牙开发】Andorid蓝牙绝对音量 的相关文章

随机推荐

  • 学习记录679@scp 拷贝当前主机某目录下某段时间内的文件到另一台服务器

    要按拷贝当前服务器的下的某个文件夹下的某段时间内的文件到另一台服务器 需要结合find exec scp命令 如下 我在当前主机下执行 拷贝此目录下的时间介于2020 12 24和2020 12 31之间的文件 注意不包括2020 12 3
  • Linux软件安装管理:在VMware挂载本地iso光盘镜像、配置yum软件仓库

    在操作VMware安装Linux系统后由于安装CentOS 7的最小化安装少了一些工具 比如 ifconfig 及 netstat 等 由于没问外部在线网络环境访问下载相关依赖包 则我们需要配置离线依赖库 本次操作是在Vmware上操作的
  • 在cesium中使用3D地形数据terrain builder的打开步骤

    本来题目名字叫做 大龄无经验程序员终成正果 纪念上班第三天 后加之后再 不行 必须把这篇博文发出去了 本篇用cesium terrain builder生成cesium可以使用的地形数据并用cesium terrain server发布 使
  • API函数的调用过程

    API函数的调用过程 ring3 Windows API Application Programing Interface 应用程序接口 简称API函数 Windows 有多少个API 主要是存放在C WINDOWS system32下面所
  • 如何理解面向对象编程(OOP)

    想要理解OOP 首先需要清楚什么是对象 所谓对象就是由一组数据结构和处理它们的方法组成的 划重点 数据 包括对象的特性 状态等的静态信息 方法 也就是行为 包括该对象的对数据的操作 功能等能动信息 把相同行为的对象归纳为类 类是一个抽象的概
  • python网络请求错误:ConnectionRefusedError: [WinError 10061] 由于目标计算机积极拒绝,无法连接。

    在用pycharm3 7做socket实验的时候 出现错误 Traceback most recent call last File D Maindocuments Mainsoftware PycharmProjects socket c
  • 逻辑左移、逻辑右移、算术左移、算术右移、循环左移、循环右移

    逻辑左移时 最高位丢失 最低位补0 逻辑右移时 最高位补0 最低位丢失 算术左移时 依次左移一位 尾部补0 最高的符号位保持不变 算术右移时 依次右移一位 尾部丢失 符号位右移后 原位置上复制一个符号位 循环左移时 将最高位重新放置最低位
  • docker mysql镜像有那几个版本

    Docker MySQL 镜像有几个版本可供选择 例如 MySQL 8 0 MySQL 5 7 MySQL 5 6 MySQL 5 5 你可以在 Docker Hub 上查看最新的 MySQL 镜像版本
  • Dell R710 iDRAC6 远程控制卡设置

    IPMI设置 设置服务器主板BIOS 以启用 iDRAC6 控制卡 启用iDRAC6 控制卡 配置 IP 用户名 密码 默认情况下 启用的 iDRAC6 网络界面使用静态 IP 地址 192 168 0 120 必须对其进行配置 才能访问i
  • day13 栈与队列

    LeetCode 239 力扣 维护一个单调队列 入队列时 保证单调递减 可以将小于待入队的数全部移除 出队列 如果不是队首出 最大元素 无需处理 package algor trainingcamp import java util De
  • python stock query

    AKShare is an elegant and simple financial data interface library for Python built for human beings 开源财经数据接口库 可以画线 GitHu
  • stm32+hx711+蓝牙hc05 称重系统(蓝牙电子秤)

    stm32 称重模块hx711 蓝牙模块hc05 本项目使用主控stm32f103c8t6 称重模块hx711 蓝牙模块hc05上传至手机app 电脑app显示数值 模块 1 stm32f103c8t6最小系统板 2 hx711 HX711
  • DataStore入门及在项目中的使用

    首先给个官网的的地址 应用架构 数据层 DataStore Android 开发者 Android Developers 小伙伴们可以直接看官网的资料 本篇文章是对官网的部分细节进行补充 一 为什么要使用DataStore 代替Shared
  • 起飞!8个 Python 加速运行骚操作

    转自 网络 本次分享纯Python编程的加速运行方法 Python 是一种脚本语言 相比 C C 这样的编译语言 在效率和性能方面存在一些不足 但是 有很多时候 Python 的效率并没有想象中的那么夸张 本文对一些 Python 代码加速
  • 2020年09月 C/C++(三级)真题解析#中国电子学会#全国青少年软件编程等级考试

    C C 编程 1 8级 全部真题 点这里 第1题 铺砖 对于一个2行N列的走道 现在用12 22的砖去铺满 问有多少种不同的方式 时间限制 3000 内存限制 131072 输入 整个测试有多组数据 请做到文件底结束 每行给出一个数字N 0
  • 常见三方结算周期都是有哪些?

    T1 为工作日次日 就是一个工作日结算 如遇到节假日 则延迟到节假日结束后第一个工作日结算 T0 为工作日当天结算 当然如遇节假日 节假日中的交易不结算 延迟到节假日结束后的第一个工作日统一结算 D1 为交易后自然日次日结算 包含节假日内
  • Streamlit 讲解专栏(三):两种方案构建多页面

    文章目录 1 前言 2 第一种方案 使用Session State实现多页面交互 2 1 Session State简介 2 2 多页面应用的基本结构 2 3 实现多页面交互的代码示例 2 4 Session State机制的优缺点 3 第
  • 如何实现 Array 和 List 之间的转换?

    在 Java 中 我们可以通过以下方法实现 Array 和 List 之间的转换 数组转 List String arr apple banana orange List
  • 2023最新宝塔面板8.0.1企业版开心版

    宝塔面板是目前一个非常好用的可视化面板 这几天我自己搭建了一个宝塔云端 不经过宝塔官方接口 无需绑定手机号 安装之后直接显示企业版 所有插件全部免费使用 付费的也能使用 脚本如下 centos安装脚本 yum install y wget
  • 【蓝牙开发】Andorid蓝牙绝对音量

    蓝牙绝对音量相关基础知识 1 什么是绝对音量 Android 蓝牙部分的官方文档有如下描述 Androud Bluetooth Service 在 Android 6 0 及更高版本中 Android 蓝牙堆栈允许音频源设置绝对音量 以便用