usbip-rs/docs/superpowers/specs/2026-03-25-uac-loopback-device-design.md
Davíð Steinn Geirsson 18a413870a feat: add UAC1 loopback test device and fix endpoint attribute dispatch
Add a simulated USB Audio Class 1 loopback device for testing
isochronous transfers. Audio sent to the playback OUT endpoint
(48kHz/16-bit/stereo) is looped back to the capture IN endpoint.

- Add UsbEndpoint::transfer_type() masking bmAttributes to bits 0-1,
  fixing dispatch for isochronous endpoints with sync-type sub-bits
- Update all endpoint attribute dispatch sites across the library
- Add UacLoopbackBuffer, UacControlHandler, UacStreamOutHandler,
  UacStreamInHandler in lib/src/uac.rs
- Add build_uac_loopback_device() builder function
- Add `test_uac connect` CLI subcommand
- Add 10 unit tests covering buffer, descriptors, and handler behavior
- Add design spec and implementation plan docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 01:43:31 +00:00

8.5 KiB

UAC1 Loopback Test Device Design

Overview

Add a simulated USB Audio Class 1 (UAC1) loopback device to verify isochronous transfer functionality. The device has one playback output (ISO OUT) and one capture input (ISO IN). Audio sent to the playback endpoint is looped back and returned from the capture endpoint.

Audio format: 48kHz, 16-bit, stereo (192 bytes/frame at 1ms full-speed intervals).

Module Structure

File: lib/src/uac.rs

Three handler structs with a shared loopback buffer:

UacLoopbackBuffer

Shared ring buffer connecting playback OUT to capture IN.

  • Inner storage: Mutex<VecDeque<u8>>
  • Capacity: ~960 bytes (5 frames, absorbs jitter without unbounded growth)
  • write(data: &[u8]) — appends; drops oldest samples if over capacity
  • read(len: usize) -> Vec<u8> — drains up to len bytes, pads with silence (zeros) on underrun

UacControlHandler

Handles the AudioControl interface (interface 0).

  • EP0 class-specific control requests: returns STALL (status = -32 / -EPIPE) for all unknown/unsupported requests. No Feature Unit in the topology means no volume/mute requests to handle.
  • get_class_specific_descriptor() — returns the AC Header + Input/Output Terminal chain.

UacStreamOutHandler (playback)

Handles AudioStreaming interface 1, alt setting 1 (ISO OUT).

  • ISO OUT: extracts audio data from each ISO packet, writes to UacLoopbackBuffer. Returns success descriptors (actual_length = length per packet).
  • get_class_specific_descriptor() — returns AS General + Format Type I descriptor.
  • set_alt_setting() — clears the loopback buffer on alt switch to prevent stale data.

UacStreamInHandler (capture)

Handles AudioStreaming interface 2, alt setting 1 (ISO IN).

  • ISO IN: reads from UacLoopbackBuffer to fill each ISO packet. On underrun, fills with silence (zeros). Returns descriptors with actual_length set to packet size.
  • get_class_specific_descriptor() — returns AS General + Format Type I descriptor.

Builder Function

pub fn build_uac_loopback_device() -> std::io::Result<UsbDevice>

Constructs the full UsbDevice with all three interfaces, alt settings, descriptors, and wired-up handlers. Keeps descriptor complexity out of the CLI.

Library Fix: EndpointAttributes transfer type masking

The EndpointAttributes enum (values 0-3) represents the base transfer type, but per the USB spec, bmAttributes encodes sync type and usage type in bits 2-5 for isochronous endpoints (e.g., 0x0D = isochronous adaptive, 0x05 = isochronous async). The library currently compares or parses ep.attributes directly against the enum, which fails when sub-bits are set.

Fix: Add a transfer_type() method to UsbEndpoint that masks to bits 0-1:

impl UsbEndpoint {
    pub fn transfer_type(&self) -> Option<EndpointAttributes> {
        FromPrimitive::from_u8(self.attributes & 0x03)
    }
}

Update all dispatch sites to use ep.transfer_type() instead of FromPrimitive::from_u8(ep.attributes) or ep.attributes == EndpointAttributes::X as u8:

  • device.rs:377 — main URB dispatch (FromPrimitive::from_u8(ep.attributes))
  • host.rs — nusb and rusb handler comparisons (ep.attributes == ...)
  • cdc.rs:59 — CDC handler comparison
  • util.rs:123 — ISO loopback test handler comparison

This allows the UAC device to use spec-compliant attribute values while keeping existing devices (which use base values like 0x01) working unchanged.

USB Descriptor Topology

Device Level

  • device_class: 0x00 (per-interface)
  • vendor_id: 0x1234, product_id: 0x5678 (arbitrary test values)
  • usb_version: 2.0, device_bcd: 1.0
  • Speed: Full-speed (UsbSpeed::Full as u32, UAC1 isochronous at 1ms intervals)

Interface 0 — AudioControl

  • Alt 0 only (no alt settings needed)
  • class=0x01 (Audio), subclass=0x01 (AudioControl), protocol=0x00
  • No endpoints (AC interrupt endpoint is optional in UAC1)
  • Class-specific descriptor chain:
    • AC Header — links to streaming interfaces 1 and 2, wTotalLength=52 (10+12+9+12+9)
    • Input Terminal 1 (USB streaming, ID=1) — receives playback from host
    • Output Terminal 2 (speaker, ID=2) — source=IT1
    • Input Terminal 3 (microphone, ID=3) — capture source
    • Output Terminal 4 (USB streaming, ID=4) — source=IT3

Interface 1 — AudioStreaming OUT (playback)

  • Alt 0: zero-bandwidth (no endpoints, required by UAC1 spec)
  • Alt 1: active
    • class=0x01 (Audio), subclass=0x02 (AudioStreaming), protocol=0x00
    • Class-specific: AS General (terminal link=IT1) + Format Type I (48kHz, 16-bit, stereo)
    • Endpoint: 0x01 (OUT), isochronous adaptive, bmAttributes=0x0D, max_packet_size=192, interval=1

Interface 2 — AudioStreaming IN (capture)

  • Alt 0: zero-bandwidth (no endpoints)
  • Alt 1: active
    • Same class/subclass/protocol as interface 1
    • Class-specific: AS General (terminal link=OT4) + Format Type I (48kHz, 16-bit, stereo)
    • Endpoint: 0x82 (IN), isochronous async, bmAttributes=0x05, max_packet_size=192, interval=1

Handler Behavior Details

Control Request Handling

The AudioControl interface has no Feature Unit (no volume/mute controls). All class-specific control requests receive a STALL response (status = -32). The Linux audio driver probes for optional features and handles STALLs gracefully.

Loopback Data Flow

Host playback OUT → UacStreamOutHandler → UacLoopbackBuffer → UacStreamInHandler → Host capture IN
  • OUT handler writes samples into the ring buffer
  • IN handler reads samples from the ring buffer
  • Buffer overflow: oldest samples dropped, newest preserved
  • Buffer underrun: silence (zeros) returned

Alt Setting Transitions

UAC1 requires zero-bandwidth alt setting 0 for streaming interfaces. The host driver activates alt setting 1 to start streaming. On set_alt_setting(), the OUT handler clears the loopback buffer.

Both alt 0 and alt 1 of each streaming interface share the same handler Arc. This is required because set_alt_setting() is called on the current active handler before the switch — sharing the Arc ensures the notification reaches the right handler regardless of which alt setting is active.

CLI Subcommand

File: cli/src/test_uac.rs

usbip-rs test_uac connect <address>

Follows the test_hid pattern:

  1. Calls usbip_rs::uac::build_uac_loopback_device()
  2. Connects to client via transport (vsock/tcp/fc-vsock)
  3. Sends OP_REP_IMPORT handshake
  4. Runs handle_urb_loop

No background task needed — the loopback is entirely driven by the host's URBs.

File: cli/src/main.rs — add TestUac variant to the clap subcommand enum.

Public API Surface

Exported from lib/src/lib.rs via pub mod uac:

  • UacLoopbackBuffer — for test injection/inspection
  • UacControlHandler
  • UacStreamOutHandler
  • UacStreamInHandler
  • build_uac_loopback_device() -> std::io::Result<UsbDevice>

All public for test suite reuse. Tests can construct individual handlers with a shared buffer and drive them directly without building the full device.

Testing

Unit tests in lib/src/uac.rs:

  • desc_verify — validates each handler's class-specific descriptor via verify_descriptor()
  • loopback_buffer_write_read — write data, read back, verify match
  • loopback_buffer_underrun — read from empty buffer, verify silence
  • loopback_buffer_overflow — write beyond capacity, verify oldest dropped, newest preserved
  • stream_out_handler_iso — ISO OUT URB request, verify success response
  • stream_in_handler_iso — write to buffer then ISO IN request, verify data returned
  • stream_in_handler_silence — ISO IN with empty buffer, verify zero-filled packets
  • control_handler_stall — unknown class request, verify STALL status (-32)

No integration tests in this scope — those will come when the broader test suite is built.

Files Changed

File Change
lib/src/endpoint.rs Add transfer_type() method to UsbEndpoint
lib/src/device.rs Use ep.transfer_type() in URB dispatch
lib/src/host.rs Use ep.transfer_type() in handler comparisons
lib/src/cdc.rs Use ep.transfer_type() in handler comparison
lib/src/util.rs Use ep.transfer_type() in ISO loopback handler
lib/src/uac.rs New — handlers, buffer, builder, tests
lib/src/lib.rs Add pub mod uac
cli/src/test_uac.rs New — CLI subcommand
cli/src/main.rs Add TestUac subcommand variant