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>
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 capacityread(len: usize) -> Vec<u8>— drains up tolenbytes, 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 = lengthper 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
UacLoopbackBufferto fill each ISO packet. On underrun, fills with silence (zeros). Returns descriptors withactual_lengthset 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 comparisonutil.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
- AC Header — links to streaming interfaces 1 and 2,
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:
- Calls
usbip_rs::uac::build_uac_loopback_device() - Connects to client via transport (vsock/tcp/fc-vsock)
- Sends OP_REP_IMPORT handshake
- 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/inspectionUacControlHandlerUacStreamOutHandlerUacStreamInHandlerbuild_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 viaverify_descriptor()loopback_buffer_write_read— write data, read back, verify matchloopback_buffer_underrun— read from empty buffer, verify silenceloopback_buffer_overflow— write beyond capacity, verify oldest dropped, newest preservedstream_out_handler_iso— ISO OUT URB request, verify success responsestream_in_handler_iso— write to buffer then ISO IN request, verify data returnedstream_in_handler_silence— ISO IN with empty buffer, verify zero-filled packetscontrol_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 |