Android无驱usb音频实现

实现android系统通过usb麦克风采集声音功能,能够兼容多款anroid设备。

设想方案有两个:
1.采用通过libusb库,直接访问usb驱动,分析usb协议中的音频数据。
2.通过tinyalsa访问音频设备的pcm节点,通过节点直接获取音频数据。

因第二种方式音频节点id并不可控,并不能适配多款android设备,遂采用第一种方式。

通过代码调用libusb库,通过usb驱动获取数据,然后封装成jni库,供apk调用。

背景知识

每一个usb设备都有自己固定的VID和PID地址,根据这个地址可以寻找到指定的usb设备。

usb有config,然后下面有多个interface,interface下面有多个endpoint。根据interface的class和subclass值可以区分interface类型,比如video的class值是14,audio的class值是1等,根据这个可以识别复合设备的interface。然后每个interface下面有多个endpoint,endpoint存在address,这个是数据传输的通道。每个endpoint存在不同的数据格式,比如我手上的这个usb麦克风,每个endpoint对应一种格式,比如双通道/16位/48K。但也有一个endpoint对应多种格式的。

usb的传输类型有四种,分别是控制传输(Control Transfer)、中断传输(Interrupt Transfer)、批量传输(Bulk Transfer)、同步传输(Isochronous Transfer)。音频设备只用到控制传输和同步传输。

代码

1.初始化

先进行init,然后根据vid、pid查找usb设备,进行open,然后因为要使用无驱设计,需要将音频alsa和usb驱动进行解绑。最后扫描所有config,根据所需要的class找到所需的interface,进行选择设置。

int UsbAudio::open(int vid, int pid, int fd, const char *usbfs)
{
    int r;                         //for return values  
    ssize_t cnt;                   //holding number of devices in list  

    printf("start open %d\n", errno);
    if (mUsbFs)
        free(mUsbFs);
    
    mUsbFs = strdup(usbfs);
    printf("before 11111 errno:%d\n", errno);
    r = libusb_init2(&ctx, usbfs);         //initialize a library session  
    if(r < 0) {  
        printf("Init Error \n"); //there was an error  
        return -1;  
    }  

    usb_dev = libusb_find_device(ctx, vid, pid, NULL, fd);

    if (usb_dev) {
        libusb_set_device_fd(usb_dev, fd);  // assign fd to libusb_device for non-rooted Android devices
        libusb_ref_device(usb_dev);
    }

    printf("before 444444 errno:%d\n", errno);
    r = libusb_open(usb_dev, &dev_handle);
    if(r != LIBUSB_SUCCESS)
    {
        printf("open device err %d\n", errno);
        return -1;
    }

    //libusb_reset_device(dev_handle);
    printf("before scan_audio_interface errno:%d\n", errno);
    if (scan_audio_interface(usb_dev) < 0)
    {
        printf("scan_audio_interface err: errno:%d\n", errno);
        cancel();
        return -1;
    }
    printf("before interface_claim_if errno:%d\n", errno);

    interface_claim_if(dev_handle, interfaceNumber);
}

int UsbAudio::interface_claim_if(libusb_device_handle *dev, int interface_number)
{
    int r = 0;

    r = libusb_kernel_driver_active(dev, interface_number);
    printf("libusb_kernel_driver_active2 %d\n", r);  
    if(r == 1) 
    { //find out if kernel driver is attached  
        printf("Kernel Driver Active\n");  
        if(libusb_detach_kernel_driver(dev, interface_number) == 0) //detach it  
            printf("Kernel Driver Detached!\n");  
    }  
    printf("kernel detach errno:%d\n", errno);

    r = libusb_claim_interface(dev, interface_number);            //claim interface 0 (the first) of device (mine had jsut 1)  
    if(r != 0) {  
        printf("Cannot Claim Interface\n");  
        return 1;  
    }  
    printf("claim_interface errno:%d\n", errno);

    return 0;
}

2.配置

设置相关属性,如采样率,interface下面的AlternateSetting等。

    ret = libusb_set_interface_alt_setting(dev_handle, interfaceNumber, 0);
    if (ret != 0) {
        printf("libusb_set_interface_alt_setting failed: %d: %s\n", interfaceNumber, libusb_error_name(ret));
        return -1;
    }

    printf("Select the altsetting, interfaceNumber:%d, altsetting:%d\n", interfaceNumber, AlternateSetting);
    ret = libusb_set_interface_alt_setting(dev_handle, interfaceNumber, AlternateSetting);
    if (ret != 0) {
        printf("libusb_set_interface_alt_setting failed: %d, %d: %s\n", interfaceNumber, 
        AlternateSetting, libusb_error_name(ret));
        return -1;
    }

    ret = libusb_control_transfer(dev_handle, LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_ENDPOINT, 
                            0x01, 0x0100, EndpointAddress, 
                            rate, sizeof(rate), 0);
    if (ret == sizeof(rate)) 
    {
        printf("set mic config success:0x%x:0x%x:0x%x\n", 
                        rate[0], rate[1], rate[2]);
    } 
    else 
    {
        printf("set mic config fail %d\n", ret);
        return ret;
    }

这些相关参数,可以根据uac协议解析判断,这里直接是抓包获取的参数,然后直接设置的。windows抓包可以用Bus Hound工具,linux驱动抓包工具可以使用usbmon。

  12.0  CTL    01 0b 00 00  03 00 00 00  SET INTERFACE         1963.1.0        
  12.0  CTL    01 0b 04 00  03 00 00 00  SET INTERFACE         1964.1.0        
  12.0  CTL    22 01 00 01  82 00 03 00  SET CUR               1965.1.0        
  12.0  OUT    80 bb 00                  ...                   1965.2.0        
  12.2  ISOC   00 00 00 00  00 00 00 00  ........              1966.1.0        
               00 00 00 00  00 00 00 00  ........              1966.1.8        
               00 00 00 00  00 00 00 00  ........              1966.1.16       
               00 00 00 00  00 00 00 00  ........              1966.1.24       
  12.2  ISOC   55 00 55 00  5c 00 5c 00  U.U.\.\.              1967.1.0        
               5c 00 5c 00  5f 00 5f 00  \.\._._.              1967.1.8        
               5c 00 5c 00  58 00 58 00  \.\.X.X.              1967.1.16       
               61 00 61 00  65 00 65 00  a.a.e.e.              1967.1.24       
  12.2  ISOC   d6 ff d6 ff  df ff df ff  ........              1968.1.0        
               e7 ff e7 ff  ed ff ed ff  ........              1968.1.8        
               e7 ff e7 ff  e9 ff e9 ff  ........              1968.1.16       
               e2 ff e2 ff  e3 ff e3 ff  ........              1968.1.24       

3.获取数据

数据通过tranfers进行传输,先通过libusb_fill_iso_transfer进行填充,其中endpointaddress即之前config扫描获取的,callback函数即是数据回调接口,然后一次libusb_submit_transfer对应一次回调,数据即在回调接口中获取。同时要创建libusb_handle_events处理的线程,这个线程是在后台调度,保证callback回调的。


    endpoint_bytes_per_packet = PackSize;
    packets_per_transfer = PackNum;
    total_transfer_size = PackSize*PackNum;

    printf("Set up the transfers\n");

    printf("before fill EndpointAddress:%d, per_packet:%d, packets:%d, total_transfer_size:%d\n", 
                       EndpointAddress, endpoint_bytes_per_packet, packets_per_transfer, total_transfer_size);
    for (transfer_id = 0; transfer_id < LIBUAC_NUM_TRANSFER_BUFS; ++transfer_id) 
    {
        printf("fill transfer_id:%d\n", transfer_id);
        transfer = libusb_alloc_transfer(packets_per_transfer);
        transfers[transfer_id] = transfer;
        transfer_bufs[transfer_id] = (unsigned char *)malloc(total_transfer_size);
        memset(transfer_bufs[transfer_id], 0, total_transfer_size);
        libusb_fill_iso_transfer(transfer, dev_handle,
            EndpointAddress,
            transfer_bufs[transfer_id], total_transfer_size,
            packets_per_transfer, _uac_stream_callback,
            (void *)NULL, 1000);

        libusb_set_iso_packet_lengths(transfer, endpoint_bytes_per_packet);
        
    }

    printf("before submit errno:%d\n", errno);
    for (transfer_id = 0; transfer_id < LIBUAC_NUM_TRANSFER_BUFS; transfer_id++) {
        printf("submit transfer_id:%d\n", transfer_id);
        ret = libusb_submit_transfer(transfers[transfer_id]);
        if (ret != 0) {
            printf("libusb_submit_transfer failed: %d, errno:%d\n", ret, errno);
            break;
        }
        printf("submit transfer_id:%d finish\n", transfer_id);
    }
    printf("after submit errno:%d\n", errno);

    thread_running = 1;

    //pthread_create(&user_thread, NULL, fill_user_frame, NULL);
    pthread_create(&event_thread, NULL, _uac_handle_events, (void*) ctx);

void *_uac_handle_events(void *args)
{
    libusb_context *handle_ctx = (libusb_context *)args;
    printf("%s start\n", __func__);
    while(UsbAudio::thread_running)
    {
        if (libusb_handle_events(handle_ctx) != LIBUSB_SUCCESS) {
            printf("libusb_handle_events err\n");
            break;
        }
    }
    printf("libusb_handle_events exit\n");
}
void _uac_stream_callback(struct libusb_transfer *transfer) 
{

    //printf("do callback\n");
    switch (transfer->status) 
    {
        case LIBUSB_TRANSFER_COMPLETED:
            if (transfer->num_iso_packets) {
                /* This is an isochronous mode transfer, so each packet has a payload transfer */
                _uac_process_payload_iso(transfer);
            }
            break;
        case LIBUSB_TRANSFER_NO_DEVICE:
            UsbAudio::running = 0;  // this needs for unexpected disconnect of cable otherwise hangup
            // pass through to following lines
        case LIBUSB_TRANSFER_CANCELLED:
        case LIBUSB_TRANSFER_ERROR:
            break;
        case LIBUSB_TRANSFER_TIMED_OUT:
        case LIBUSB_TRANSFER_STALL:
        case LIBUSB_TRANSFER_OVERFLOW:
            break;
    }

    if (UsbAudio::thread_running) 
    {
        //printf("libusb_submit_transfer next.\n");
        if (libusb_submit_transfer(transfer) < 0) {
            printf("libusb_submit_transfer err.\n");
        }
    } 

}
void _uac_process_payload_iso(struct libusb_transfer *transfer) {
    /* per packet */
    unsigned char *pktbuf;
    size_t header_len;
    unsigned char header_info;
    struct libusb_iso_packet_descriptor *pkt;
    
    int packet_id;
    unsigned char* recv = (unsigned char*)malloc(PACKET_SIZE*NUM_PACKETS);//(PACKET_SIZE * transfer->num_iso_packets);
    unsigned char* recv_next = UsbAudio::holdbuf;
    int len = 0;

    if (transfer->type != LIBUSB_TRANSFER_TYPE_ISOCHRONOUS) 
    {
        printf("not isoc packet\n");
        return;
    }

    //printf("record pcm,isonum %d, len %d, actlen %d\n", transfer->num_iso_packets,transfer->length,transfer->actual_length);
    for (packet_id = 0; packet_id < transfer->num_iso_packets; ++packet_id) {

        pkt = &transfer->iso_packet_desc[packet_id];

        if (pkt->status != LIBUSB_TRANSFER_COMPLETED) {
            printf("bad packet:status=%d,actual_length=%d", pkt->status, pkt->actual_length);
            continue;
        }

        
        pktbuf = libusb_get_iso_packet_buffer_simple(transfer, packet_id);
        if(pktbuf == NULL)
        {
            printf("receive pktbuf null\n");
        }
        
        memcpy(recv_next, pktbuf, pkt->length);
        recv_next += pkt->length;
        len += pkt->length;
    
    }   // for

    //在这里进行数据保存和拷贝
    if (fwrite(recv, 1, len, pFile) < 0) {
        perror("Unable to write to descriptor");
    }

    free(recv);

   
    if (len > UsbAudio::PackSize * transfer->num_iso_packets) {
        printf("Error: incoming transfer had more data than we thought.\n");
        return;
    }


}

另外需要一个注意的地方是endpoint_bytes_per_packet是每个包长,该值必须比endpoint支持的最大包长要小。packets_per_transfer是包数,这个值其实另有含义,同时也是采样延时的时间。比如endpoint_bytes_per_packet设置为384,而packets_per_transfer设置为10,那么callback的时间就是10ms后返回3840byte数据。

总结

该程序其实是简单的获取指定usb麦克设备数据的。
其实可以做的更有兼容一些,比如扫描config/interface Descriptor,可以获取相关设备描述,区分是否存在音频设备。根据UAC相关协议进行封装,可以获取interface和endpoint的usb协议extra扩展部分,根据该部分信息,解析可以获取设备支持配置、endpoint长度地址等。

同类项目,针对usbcamera相关的无驱设计,可以参考git上的开源项目。该项目将UVC协议进行了封装。
UVCCamera

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,527评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,314评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,535评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,006评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,961评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,220评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,664评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,351评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,481评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,397评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,443评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,123评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,713评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,801评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,010评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,494评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,075评论 2 341