Search code examples
linuxusbalsa

Obtain USB Device File Path from ALSA USB Hardware Device


I'm currently working on a Rust library/CLI/UI for managing Focusrite Scarlett USB audio devices specifically on Linux. These devices often have many different internal settings (my 18i8 has nearly 300 individual controls), so I'm trying to account for all of these in my design to make it easy for users to change routing configuration, input/output/mix gain, mute toggles, etc.

Thankfully, Linux already supports these devices with a bit of modprobe configuration and a recent kernel:

/etc/modprobe.d/scarlett.conf:

options snd_usb_audio vid=0x1235 pid=0x8214 device_setup=1

For different models and possibly different major hardware versions, the above values need to be set to different USB product IDs to enable support for those devices as well.

I have been studying the ALSA API for what feels like weeks now, and I've learned quite a lot:

  1. Someone wrote a GTK4 UI in C for managing Scarlett devices, and I have basically read the entire codebase and in spite of all of the pain, I was able to figure out what exactly it is doing/which ALSA APIs are being utilized.
  2. The ALSA documentation is abysmal at best, so after this project I will be writing blog posts describing my woes and explaining the subtleties, as well as attempting to contribute back to the ALSA documentation to save others days/weeks/months of their lives trying to figure these things out.
  3. The ALSA userspace library is often simply typedefs of syscalls to the kernel, meaning that for things like the snd_ctl_t, snd_hctl_t, and snd_mixer_t APIs, there is essentially no code in userspace. All structs are opaque, and functions (syscalls) are used with these struct references to obtain details. The kernel does some magic with memory and gives it to userspace.
  4. Thankfully, a brave and generous soul has ported most of the ALSA API to Rust. I will also be contributing documentation to this project to help others along their way.

In any case, here is my predicament: I can list ALSA devices and iterate over them, and via a snd_ctl_card_info_t I can get a few properties which are helpful like:

  • name: Scarlett 18i8 USB
  • id: USB
  • longname: Focusrite Scarlett 18i8 USB at usb-0000:00:14.0-2.2, high speed
  • mixername: USB Mixer
  • components: USB1235:8214
  • driver: USB-Audio

However, this seems to be the most I can get out of the available APIs. I can get the model type by extracting that from the USB vendor/product IDs in the components field, and I can perhaps find the actual USB handle by parsing the longname, but:

  1. There may be multiple devices connected of the same USB vendor/product IDs: this is a common technique to chain devices together to add inputs/outputs.
  2. I'm concerned about the stability of extracting the usb-* slug and the USB vendor/product IDs, perhaps this may change with kernel versions and become invalid.
  3. I need to identify each device, hopefully with a serial number of some kind, so that configuration for card A (18i8) will never be inadvertently applied to card B (18i8), and I assume there is some sort of USB device field which contains a serial number for each device.

Therefore, is there a Linux API which will allow me to get a USB device file from an ALSA handle of some kind? Alternatively, is there something I may be missing which will allow getting the serial number from within the ALSA API?


EDIT: I'm able to find the serial number via lsusb, the field name is iSerial:

$ sudo lsusb -d 1235:8214 -vv | grep iSerial
  iSerial          2 4K1A0P443EPEW7

It does seem that I need to be root to get the actual field value, but I can probably fix this by granting permissions to the device to my user in udev rules. If I can only get the stable USB device path from an ALSA API, I can then use libusb or another API to extract the iSerial field.


Solution

  • Use the index of the card, as returned by snd_ctl_card_info_get_card. The card index is the number that appears in the name of the sound card’s /dev/snd/controlC⟨idx⟩ device node (and other device nodes associated with the card in /dev/snd/). From this, you can obtain the corresponding node in the sysfs tree and walk the chain of parent nodes to obtain any identifier you want; either by opening files under /sys directly or going via udev.

    Below is a demonstration using PyALSA and GUdev bindings to Python, but you should be able to write something analogous in any language with bindings to ALSA (libasound) and udev (libudev or GUdev).

    #!/usr/bin/env python3
    import gi
    gi.require_version('GUdev', '1.0')
    from gi.repository import GUdev
    from pyalsa import alsacard
    
    client = GUdev.Client()
    
    for index in alsacard.card_list():
        print(f'index={index!r} longname={alsacard.card_get_longname(index)!r}')
    
        device = client.query_by_subsystem_and_name('sound', 'controlC' + str(index))
        usb_device = device.get_parent_with_subsystem('usb', 'usb_device')
        if not usb_device:
            print('    seems not to be an USB device')
            continue
    
        vidpid = f'''{
            usb_device.get_sysfs_attr('idVendor')
        }:{
            usb_device.get_sysfs_attr('idProduct')
        }'''
        serial = usb_device.get_sysfs_attr('serial')
    
        print(f'    VID:PID={vidpid} serial={serial!r}')