zoukankan      html  css  js  c++  java
  • 蓝牙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
    ");
            return -1;
        }
    
        /* 3 Listen to netlink socket */
        if (bind(pfd.fd, (void *)&nls, sizeof(struct sockaddr_nl)))
        {
            printf("Failed to bind socket
    ");
            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
    ");
                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
    ", 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
    ");
        			 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
    ");
                if (audio_fd >0) {
                    close(audio_fd);
                    audio_fd = -1;
                }
                close_upcm_dev();
            } else if (buf[1] == 0xFE ) {
                printf("Recv prevIndex and prevSample
    ");
        			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

    参考

  • 相关阅读:
    Handling Configuration Changes with Fragments
    Android基础——Fragment与Activity交互
    Android基础——Fragment与Activity交互
    Activity和Fragment生命周期对比
    Activity和Fragment生命周期对比
    移动端与PHP服务端接口通信流程设计(基础版)
    移动端与PHP服务端接口通信流程设计(基础版)
    ROW/COW 快照技术原理解析
    ROW/COW 快照技术原理解析
    OpenStack 实现技术分解 (5) 应用开发 — 使用 OpenStackClients 进行二次开发
  • 原文地址:https://www.cnblogs.com/zozo825117/p/10272395.html
Copyright © 2011-2022 走看看