最近公司一个项目,主要是想实现高质量音频的录制。虽然说目前大部分手机的录音质量都是可以的,但是想要录制高质量,立体声的音频,一般的手机还是望成莫及的。为了实现192K的立体声录音,电子组的同事直接搞了一个USB声卡过来让我获取其中的音频数据。

心想USB声卡应该算是一个通用的器件,大部分系统都应该可以直接使用了吧。于是找了几款手机,通过OTG线连接测试一下,测试结果发现只有TypeC接口的手机可以直接使用,而MicroUSB接口的手机则无法使用它录音。没办法,只好自己通过USB读取声卡的音频数据。

USB协议是一项很复杂的协议,有兴趣的可以自行查找相关资料学习。这里,我简单提一下和我们USB声卡相关的几点:

  1. 每一个usb设备都有自己固定的VID和PID地址,根据这个地址可以寻找到指定的usb设备。这个VID和PID可以在枚举USB设备的时候获取到。可参考UsbDevice中的方法。
  2. USB设备有config,下面有多个interface,interface下面有多个endpoint。interface的Class值可以区分interfce类型,音频设备的interface Class值是1。每一个interface下面有多个endpoint,每一个endpoint有一个唯一的Address,这个就是数据通信的通道了。
  3. USB的传输类型有四种,分别是控制传输(Control Transfer)、中断传输(Interrupt Transfer)、批量传输(Bulk Transfer)、同步传输(Isochronous Transfer)。音频设备只用到控制传输和同步传输。
  4.  

提到USB通信,我首先想到的是可以利用AndroidSDK提供的USB接口进行数据通信。使用SDK通过的API操作USB很简单,主要的类是UsbManager、UsbEndpointUsbInterfaceUsbDeviceConnectionUsbDevice。请看我的例子代码:

public class MainActivity extends AppCompatActivity {

    private UsbManager mUsbManager;
    public static final String ACTION_DEVICE_PERMISSION = "com.linc.USB_PERMISSION";
    private PendingIntent mPermissionIntent;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mUsbManager = (UsbManager)getSystemService(Context.USB_SERVICE);

        // 用于USB授权的Intent
        mPermissionIntent = PendingIntent.getBroadcast(this,0,new Intent(ACTION_DEVICE_PERMISSION),0);

        // 枚举USB设备
        HashMap<String,UsbDevice> deviceHashMap = mUsbManager.getDeviceList();
        Iterator<UsbDevice> iterator = deviceHashMap.values().iterator();
        while (iterator.hasNext()) {
            UsbDevice device = iterator.next();
            // 判断APP对该USB设备是否有权限,若没有权限则申请USB设备权限
            if(mUsbManager.hasPermission(device)) {
                //do your work
                initCommunication(device);
            } else {
                mUsbManager.requestPermission(device,mPermissionIntent);
            }
            Log.e("TAG", "\ndevice name: "+device.getDeviceName()+"\ndevice product name:"
                    +device.getProductName()+"\nvendor id:"+device.getVendorId()+
                    "\ndevice serial: "+device.getSerialNumber()+
                    "\ndevice serial: "+device.getProductId());
        }

    }

    @Override
    protected void onResume() {
        super.onResume();
        
        // 注册USB设备插拔、授权等广播,以监听到之后处理相应的操作。
        IntentFilter usbFilter = new IntentFilter();
        usbFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
        usbFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
        usbFilter.addAction(ACTION_DEVICE_PERMISSION);
        registerReceiver(mUsbReceiver, usbFilter);
    }

    @Override
    protected void onPause() {
        super.onPause();
        unregisterReceiver(mUsbReceiver);
    }

    /*==============以下获取特定的USB设备信息=============*/
    private UsbEndpoint mUsbEndpointIn; 
    private UsbEndpoint mUsbEndpointOut;
    private UsbInterface mUsbInterfaceInt;
    private UsbInterface musbInterfaceOut;
    UsbDeviceConnection mUsbDeviceConnection;
    /**
     *
     * 遍历USB设备各个节点,获取节点信息,并且打开USB连接
     */
    private void initCommunication(UsbDevice device) {
        tvInfo.append("initCommunication in\n");
        tvInfo.append("initCommunication in right device\n");
        int interfaceCount = device.getInterfaceCount();
        tvInfo.append("interfaceCount " + interfaceCount + "\n");
        for (int interfaceIndex = 0; interfaceIndex < interfaceCount; interfaceIndex++) {
            UsbInterface usbInterface = device.getInterface(interfaceIndex);
            tvInfo.append("=========>>"+ usbInterface.getInterfaceClass() + usbInterface.getInterfaceSubclass()  + "<<===================\n");
            if ((UsbConstants.USB_CLASS_AUDIO != usbInterface.getInterfaceClass())
                    && (UsbConstants.USB_CLASS_HID != usbInterface.getInterfaceClass())) {
                tvInfo.append("usbInterface.getInterfaceClass() " + usbInterface.getInterfaceClass() + usbInterface.getInterfaceSubclass() + "\n");
                continue;
            }

            tvInfo.append("========="+ usbInterface.getEndpointCount() + "===================\n");
            for (int i = 0; i < usbInterface.getEndpointCount(); i++) {
                UsbEndpoint ep = usbInterface.getEndpoint(i);
                tvInfo.append("usbInterface.getEndpoint(i) " + ep.getType() + "\n");
                if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_ISOC) {
                    if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
                        mUsbEndpointIn = ep;
                        mUsbInterfaceInt = usbInterface;
                        tvInfo.append("mUsbEndpointIn \n");
                    } else {
                        mUsbEndpointOut = ep;
                        musbInterfaceOut = usbInterface;
                        tvInfo.append("mUsbEndpointOut \n");
                    }

                }
            }

        }
        if ((null == mUsbEndpointIn) || (null == mUsbEndpointOut)) {
            tvInfo.append("endpoint is null\n");
            mUsbEndpointIn = null;
            mUsbEndpointOut = null;
//            mUsbInterface = null;
        } else {
            tvInfo.append("\nendpoint out: " + mUsbEndpointOut + ",endpoint in: " +
                    mUsbEndpointIn.getAddress()+"\n");
//            mUsbInterface = usbInterface;
            mUsbDeviceConnection = mUsbManager.openDevice(device);
            mUsbDeviceConnection.claimInterface(mUsbInterfaceInt, true);
            mUsbDeviceConnection.claimInterface(musbInterfaceOut, true);
            tvInfo.append("mUsbDeviceConnection: " + byteToString(mUsbDeviceConnection.getRawDescriptors()) + "\n");
        }

    }

    /**将byte数组转换为可读性高的字符串*/
    private String byteToString(byte[] b){
        String result = "";
        for (int i = 0; i < b.length; i++){
            result = String.format("%s %02x", result, b[i]);
        }
        return result;
    }

    // USB设备插拔、授权的Receiver。
    private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            tvInfo.append("BroadcastReceiver in\n");

            if(UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
                tvInfo.append("ACTION_USB_DEVICE_ATTACHED\n");
                UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                initCommunication(device);
            } else if(UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
                tvInfo.append("ACTION_USB_DEVICE_DETACHED\n");
            }else if (ACTION_DEVICE_PERMISSION.equals(action)){
                synchronized (this) {
                    UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                    if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                        if (device != null) {
                            tvInfo.append("usb EXTRA_PERMISSION_GRANTED");
                        }
                    } else {
                        tvInfo.append("usb EXTRA_PERMISSION_GRANTED null!!!");
                    }
                }

            }
        }
    };
}

例子实现的功能就是发现USB设备获取信息,并打开连接通道。通道打开成功之后,就可以调用USB传输方法传输数据了。SDK提供了上述四种传输类型中的两种: 

/*===========控制传输=============*/
public int controlTransfer(int requestType, int request, int value, int index,
            byte[] buffer, int offset, int length, int timeout)
/*=============批量传输=================*/
public int bulkTransfer(UsbEndpoint endpoint,
            byte[] buffer, int length, int timeout)

传输方向是由endpoint决定的,一个endpoint只有一个传输方向。

Android官方文档直接指出不支持同步传输(Isochronous Transfer),而这正是我们USB声卡的传输方式,使用SDK的方式是行不通了,因此,我们需要使用JNI的方式获取音频数据。下一篇将直接介绍使用JNI的方法来连接USB。