蓝牙ble数据转语音实现Android AudioRecord方法推荐

教程 

欢迎走进zozo的学习之旅。

概述

蓝牙BLE又称bluetooth smart,主打的是低功耗和快速链接,所以在支持的profile并没有audio的部分,而蓝牙语音协议A2DP只在传统蓝牙中有,本文就是提供一种利用ble数据来传输压缩语音,并最终在实现用android语音框架中的AudioRecord方法来获取语音流。

主要思路

首先问题的需求是从一种非标准的协议挂载成为一个标准协议。那通过修改kernel的bluetooth协议或者是修改android的语音框架都是可以实现的,但是不论哪种方式都要耗费大量的工作,而且这两种的哪一种的修改都会给平台的更换或者是系统版本的更换带来很大的障碍。

那这里提供的一种较为简单的思路来实现:在kernel内建议一个upcm的声卡,运行一个守护进程将ble的对应数据解压后放入声卡这样AudioRecord就可以获取PCM的语音流了。另外,android语音的挂载需要添加so库,并修改Audio的配置文件audio_policy.conf来添加。

UPCM分析

kernel声卡驱动

upcm的源码可关注我的代码仓库

蓝牙正常 连接 log

[  633.209000] input: Broadcom Bluetooth HID as /devices/virtual/misc/uhid/input4
[ 633.217000] generic-bluetooth 0005:0000:0000.0002: input,hidraw0: BLUETOOTH HID v1.01 Mouse [Broadcom Bluetooth HID] on
[ 641.437000] UPCM : snd_u_capture_open
[ 641.440000] UPCM : snd_u_hw_params format 2, rate 16000, channels 1, period_bytes 2048, buffer_bytes 8192
[ 641.451000] UPCM: format 0x2, rate 16000, channels 1
[ 641.456000] UPCM : snd_u_pcm_prepare
[ 641.460000] UPCM : snd_u_substream_capture_trigger, cmd 1
[ 641.465000] UPCM: SNDRV_PCM_TRIGGER_START
[ 649.407000] UPCM: upcm_char_release
[ 651.592000] UPCM : snd_u_substream_capture_trigger, cmd 0
[ 651.597000] UPCM: SNDRV_PCM_TRIGGER_STOP
[ 651.602000] UPCM : snd_u_hw_free
[ 651.605000] UPCM : snd_u_capture_close

在内核路径下进行交叉编译,把编译完的upcm.ko放到文件系统/system/etc/下,在板级的init.rc里加入insmod /system/etc/upcm.ko

这样上电就可以加载upcm.ko的驱动。驱动加载成功后,会建立/sys/class/sound/pcmC1D0c的虚拟通道,设备节点在 /dev/snd/pcmC1D0c

audio daemon

Audio daemon程序,从一个socket通道获取蓝牙BLE语音数据,解压ADPCM数据,喂给一个虚拟的声卡。Android语音中间层通过一个标准的audio库,从虚拟声卡中读取音频,提供给APP使用。APP只要调用标准的Android音频API,就能获取音频数据。


  • 使用Netlink的NETLINK_KOBJECT_UEVENT类型套接字与Kernel进行通信,查找hidraw设备
int main_loop()
{
/* 套接字地址 */
struct sockaddr_nl nls;
/* 套接字文件描述符 */
struct pollfd pfd;
/* 接收内核发来的消息缓冲区大小 */
char buf[512];
/* 查找设备路径 */
char dev_path[512];
// Open hotplug event netlink socket
memset(&nls,0,sizeof(struct sockaddr_nl));
/* 1.添写套接字地址 */
nls.nl_family = AF_NETLINK;
/* 如果希望内核处理消息或多播消息,就把该字段设置为 0,
否则设置为处理消息的进程ID。 */
nls.nl_pid = getpid();
nls.nl_groups = -1;
/*设置要求查询的事件掩码 */
pfd.events = POLLIN;
/* 2.创建套接字 */
/* NETLINK_KOBJECT_UEVENT - 内核消息到用户空间*/
pfd.fd = socket(PF_NETLINK, /* 使用 netlink */
SOCK_DGRAM, /* 使用不连续不可信赖的数据包连接 */
NETLINK_KOBJECT_UEVENT);
/* 创建套接字失败 */
if (pfd.fd < 0 )
{
printf("Failed to open netlink socket\n");
return -1;
} /* 3 Listen to netlink socket */
if (bind(pfd.fd, (void *)&nls, sizeof(struct sockaddr_nl)))
{
printf("Failed to bind socket\n");
return -1;
}
/* 创建子进程,为已经存在hidraw设备的uevent事件,添加 add 关键字*/
deal_with_exist_hidraw_dev(); while (1)
{
/* 等待事件 */
int res = poll(&pfd, 1, -1) ;
if (res == -1)
{
if (errno == EAGAIN || errno == EINTR)
continue;
break;
}
/* 接收内核消息 */
int i = 0 ;
int len = recv(pfd.fd, buf, sizeof(buf), MSG_DONTWAIT);
if (len == -1 )
{
if (errno == EAGAIN || errno == EINTR)
continue;
printf("Error when recv netlink package\n");
return -1;
} i = 0 ;
char * token = buf;
char * action_token = NULL;
char * devname_token = NULL;
/* 检查消息关键字 hidraw 设备 */
while ( i<len )
{
token = buf + i;
i += strlen(token) + 1;
if (!strncmp(token, "ACTION=add", 10) )
{
action_token = token;
} if (!strncmp(token, "DEVNAME=/dev/hidraw", 19) || !strncmp(token, "DEVNAME=hidraw", 14) )
{
devname_token = token;
}
/* 找到了hidraw设备 */
if (action_token != NULL && devname_token != NULL)
{
if (!strncmp(devname_token, "/dev/", 5 ) )
strcpy(dev_path, devname_token + 8 );
else
sprintf(dev_path, "/dev/%s", devname_token + 8);
//char * dev_path = devname_token + 8;
printf("Found new hidraw device\n", dev_path);
handle_new_hidraw_dev(dev_path);
break;
}
}
} close(pfd.fd);
}
  • 选择具有约定ble语音特征的hid设备
inline bool select_device(char * dev_path)
{
int fd,res,desc_size,i;
struct hidraw_devinfo info;
struct hidraw_report_descriptor rpt_desc;
char buf[100];
/* 打开hidraw设备文件 */
fd = open(dev_path, O_RDWR);
if (fd < 0 )
{
sleep(1);
fd = open(dev_path, O_RDWR);
if (fd < 0 )
return false;
} ioctl(fd, HIDIOCGRDESCSIZE, &desc_size);
ioctl(fd, HIDIOCGRAWINFO, &info);
close(fd); int vid = info.vendor & 0xFFFF;
int pid = info.product & 0xFFFF; for (i=0;;i++)
{
struct device * dev = dev_list[i];
if (dev == NULL)
break;
/* 查找对应ble语音设备可以根据设备的特征绑定 */
if ( (dev->vid <= 0 || dev->vid == vid)
&&( dev->pid <=0 || dev->pid == pid)
&& (dev->desc_size <=0 || dev->desc_size == desc_size ) )
{
current_device = dev;
strncpy(current_device->dev_path, dev_path, sizeof(current_device->dev_path));
return true;
}
} return false;
}
/* 这里没有对vid pid做绑定,用了设备的报告描述符了做征绑定 */
struct device dev_default =
{
.name = "default",
.vid = 0,
.pid = 0,
.desc_size = 220,
.audio_main = default_audio_main
};
  • 自定义语音流控制,解压+输入到upcm
int default_audio_main(int fd)
{
int len;
short pcm_buf[1024];
int offset = 0;
unsigned char buf[1024];
int total_cnt = 0;
int i; int audio_fd = -1;
while ( (len = read (fd, buf, 1024 )) >=0) { /* 这里没有对vid pid做绑定,用了设备的报告描述符了做征绑定 */
if (buf[0] == 0x1F) {
if (buf[1] == 0xFF && buf[2] == 0x01) {
printf("Audio start\n");
if (audio_fd < 0)
audio_fd = open("audio.pcm", O_WRONLY | O_CREAT); open_upcm_dev();
} else if (buf[1] == 0xFF && buf[2] == 0x00) {
printf("Audio stopped\n");
if (audio_fd >0) {
close(audio_fd);
audio_fd = -1;
}
close_upcm_dev();
} else if (buf[1] == 0xFE ) {
printf("Recv prevIndex and prevSample\n");
state.prevIndex = buf[2];
state.prevSample = covertTo16Int(buf[3], buf[4]);
}
} else if (buf[0] == 0x1E) {
int sample_cnt = adpcm_decode(buf+1, len-1, pcm_buf);
write(audio_fd, pcm_buf, sample_cnt * 2);
write_upcm_dev((unsigned char *)pcm_buf, sample_cnt * 2);
}
} if (audio_fd > 0 )
close(audio_fd);
}
  • 编译完成后将工具audio_d放到system/bin,加入到系统里自动启动
        service hidraw /system/bin/audio_d
class main
oneshot

 注意,需要在加载upcm.ko之后运行。

注册系统声卡

  1. 编译audio的so库,Android.mk audio_hw.cpp
  2. audio.LSDAudio.default.so,放到system/lib/hw/下。
  3. 修改system/etc/audio_policy.conf 文件,把primary里的input删掉,只留output,并加上下面的内容。可参考文件包的样本,注意检查权限为644。

    audio.LSDAudio.default.so的加载,是靠audio_policy.conf里面建立PCM通道来加载的。这样就创建了一个input_mic的PCM通道。
        LSDAudio {
inputs {
LSDAudio {
sampling_rates 8000|16000
channel_masks AUDIO_CHANNEL_IN_MONO
formats AUDIO_FORMAT_PCM_16_BIT
devices AUDIO_DEVICE_IN_BUILTIN_MIC
}
}
}

源代码下载

ble_audio_android

参考

蓝牙ble数据转语音实现Android AudioRecord方法推荐的更多相关文章

  1. 蓝牙BLE数据包格式汇总

    以蓝牙4.0为例说明: BLE包格式有:广播包.扫描包.初始化连接包.链路层控制包(LL层数据包).逻辑链路控制和自适应协议数据包(即L2CAP数据包)等: 其中广播包又分为:定向广播包和非定向广播包 ...

  2. android4.3 蓝牙BLE编程

    一.蓝牙4.0简介 蓝牙4.0标准包含两个蓝牙标准,准确的说,是一个双模的标准,它包含传统蓝牙部分(也有称之为经典蓝牙Classic Bluetooth)和低功耗蓝牙部分(Bluetooth Low ...

  3. Android4.3 蓝牙BLE初步

    一.关键概念: Generic Attribute Profile (GATT) 通过BLE连接,读写属性类小数据的Profile通用规范.现在所有的BLE应用Profile都是基于GATT的.   ...

  4. 【转】Android4.3 蓝牙BLE初步

    原文网址:http://www.cnblogs.com/savagemorgan/p/3722657.html 一.关键概念: Generic Attribute Profile (GATT) 通过B ...

  5. Android-低功耗蓝牙(BLE)-客户端(主机/中心设备)和服务端(从机/外围设备)

    一.Android 低功耗蓝牙(BLE)的API简介 从Android 4.3(API 18)才支持低功耗蓝牙(Bluetooth Low Energy, BLE)的核心功能, BLE蓝牙协议是GAT ...

  6. Android蓝牙BLE开发,扫描、连接、发送和读取信息;

    1.BLE开发权限 Android蓝牙BLE开发须打开蓝牙权限和6.0位置权限: <uses-permission android:name="android.permission.B ...

  7. Android蓝牙BLE低功耗相关简单总结

    在看Android4.42的源代码时看到有加入对BLE设备的处理.看的一头雾水,多方百度,最终有种柳暗花明的感觉. 本文总结来源于百度多篇文章,欢迎转载.分享交流 BLE蓝牙概念 BLE:Blueto ...

  8. 使用BleLib的轻松搞定Android低功耗蓝牙Ble 4.0开发具体解释

    转载请注明来源: http://blog.csdn.net/kjunchen/article/details/50909410 使用BleLib的轻松搞定Android低功耗蓝牙Ble 4.0开发具体 ...

  9. 10分钟完成一个最最简单的BLE蓝牙接收数据的DEMO

    这两天在研究蓝牙,网上有关蓝牙的内容非常有限,Github上的蓝牙框架也很少很复杂,为此我特地写了一个最最简单的DEMO,实现BLE蓝牙接收数据的问题, 不需要什么特定的UUID, 不需要什么断开重连 ...

随机推荐

  1. 『ACM C++』 Codeforces | 1005D - Polycarp and Div 3

    今天佛了,魔鬼周一,在线教学,有点小累,但还好,今天AC了一道,每日一道,还好达成目标,还以为今天完不成了,最近任务越来越多,如何高效完成该好好思考一下了~最重要的还是学业的复习和预习. 今日兴趣新闻 ...

  2. 【CodeForces 129 B】Students and Shoelaces(拓扑排序)

    Anna and Maria are in charge of the math club for junior students. When the club gathers together, t ...

  3. VMware Workstation 安装Vmware tools 是 出现vmware tools unavailable

    这个问题是因为虚拟机安装的时候操作系统选择的不对,在Virtual Machine Settings中选择Options,在General中选择正确的操作系统类型 例如Guest operating ...

  4. 【淘宝客】PHPMailer-v6.0.5版 发送邮件dome

    话不多说,根据官方dome修改下,官方更新地址:https://github.com/PHPMailer/PHPMailer <?php use PHPMailer\PHPMailer\PHPM ...

  5. 查看Pyton的版本号和32/64位平台

    怎么查看Python的版本号?使用的Python是32位还是64位的?用以下两条Python 指令就可以知道. 方法1:通过Python代码查看 import platform import sys ...

  6. 安装使用supervisor来启动服务

    supervisor 使用方法 supervisor(官网)是一个unix的系统进程管理软件,可以用它来管理apache.nginx等服务, 若服务挂了可以让它们自动重启.当然也可以用来实现golan ...

  7. 成都Uber优步司机奖励政策(1月30日)

    滴快车单单2.5倍,注册地址:http://www.udache.com/ 如何注册Uber司机(全国版最新最详细注册流程)/月入2万/不用抢单:http://www.cnblogs.com/mfry ...

  8. 3991: [SDOI2015]寻宝游戏

    3991: [SDOI2015]寻宝游戏 https://www.lydsy.com/JudgeOnline/problem.php?id=3991 分析: 虚树+set. 要求树上许多点之间的路径的 ...

  9. Unbuntu安装RVM

    apt-get install curl #安装rvm curl -L https://get.rvm.io | bash #执行启动 source /home/mafei/.rvm/scripts/ ...

  10. js中call()方法和apply方法的使用

    1. 方法定义 call方法: 语法:call([thisObj[,arg1[, arg2[, [,.argN]]]]]) 定义:调用一个对象的一个方法,以另一个对象替换当前对象. 说明: call ...