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

Posted zozo825117

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了蓝牙ble数据转语音实现Android AudioRecord方法推荐相关的知识,希望对你有一定的参考价值。

蓝牙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方法推荐的主要内容,如果未能解决你的问题,请参考以下文章

Android ble (蓝牙低功耗)使用注意事项(转)

深入浅出蓝牙低功耗音频BLE Audio--4 ISO音频流

Android-Ble蓝牙开发Demo示例–扫描,连接,发送和接收数据,分包解包(附源码)

蓝牙Mesh笔记 ①ESP32-C3 模组上实现天猫精灵蓝牙 BLE Mesh AliGenie 接入,无需WiFi 连接也可以实现天猫精灵语音控制。

蓝牙Mesh笔记 ①ESP32-C3 模组上实现天猫精灵蓝牙 BLE Mesh AliGenie 接入,无需WiFi 连接也可以实现天猫精灵语音控制。

Android BLE (低功耗蓝牙)应用