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>
This commit is contained in:
Davíð Steinn Geirsson 2026-03-25 01:43:31 +00:00
parent e4cdc4beec
commit 18a413870a
14 changed files with 1970 additions and 50 deletions

View file

@ -56,7 +56,7 @@ impl UsbInterfaceHandler for UsbCdcAcmHandler {
) -> Result<UrbResponse> {
let ep = request.ep;
let req = &request.data;
if ep.attributes == EndpointAttributes::Interrupt as u8 {
if ep.transfer_type() == Some(EndpointAttributes::Interrupt) {
// interrupt
if let Direction::In = ep.direction() {
// interrupt in

View file

@ -43,7 +43,7 @@ pub enum ClassCode {
}
/// A list of defined USB endpoint attributes
#[derive(Copy, Clone, Debug, FromPrimitive)]
#[derive(Copy, Clone, Debug, FromPrimitive, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum EndpointAttributes {
Control = 0,

View file

@ -374,7 +374,7 @@ impl UsbDevice {
let setup_packet = request.setup.clone();
let out_data = request.data.clone();
match (FromPrimitive::from_u8(ep.attributes), ep.direction()) {
match (ep.transfer_type(), ep.direction()) {
(Some(Control), In) => {
// control in
debug!("Control IN setup={setup_packet:x?}");

View file

@ -28,4 +28,10 @@ impl UsbEndpoint {
pub fn is_ep0(&self) -> bool {
self.address & 0x7F == 0
}
/// Get the base transfer type from bmAttributes (bits 0-1).
/// This masks off the isochronous sync/usage sub-bits (bits 2-5).
pub fn transfer_type(&self) -> Option<EndpointAttributes> {
FromPrimitive::from_u8(self.attributes & 0x03)
}
}

View file

@ -43,7 +43,7 @@ impl UsbInterfaceHandler for RusbUsbHostInterfaceHandler {
let mut buffer = vec![0u8; transfer_buffer_length as usize];
let timeout = std::time::Duration::new(1, 0);
let handle = self.handle.lock().unwrap_or_else(|e| e.into_inner());
if ep.attributes == EndpointAttributes::Control as u8 {
if ep.transfer_type() == Some(EndpointAttributes::Control) {
// control
if let Direction::In = ep.direction() {
// control in
@ -75,7 +75,7 @@ impl UsbInterfaceHandler for RusbUsbHostInterfaceHandler {
)
})?;
}
} else if ep.attributes == EndpointAttributes::Interrupt as u8 {
} else if ep.transfer_type() == Some(EndpointAttributes::Interrupt) {
// interrupt
if let Direction::In = ep.direction() {
// interrupt in
@ -94,7 +94,7 @@ impl UsbInterfaceHandler for RusbUsbHostInterfaceHandler {
)
})?;
}
} else if ep.attributes == EndpointAttributes::Bulk as u8 {
} else if ep.transfer_type() == Some(EndpointAttributes::Bulk) {
// bulk
if let Direction::In = ep.direction() {
// bulk in
@ -110,7 +110,7 @@ impl UsbInterfaceHandler for RusbUsbHostInterfaceHandler {
)
})?;
}
} else if ep.attributes == EndpointAttributes::Isochronous as u8 {
} else if ep.transfer_type() == Some(EndpointAttributes::Isochronous) {
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"isochronous transfers not supported on rusb handler",
@ -236,7 +236,7 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
_ => std::time::Duration::from_secs(1), // control, bulk
};
let handle = self.handle.lock().unwrap_or_else(|e| e.into_inner());
if ep.attributes == EndpointAttributes::Control as u8 {
if ep.transfer_type() == Some(EndpointAttributes::Control) {
// control
if let Direction::In = ep.direction() {
// control in
@ -307,7 +307,7 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
.wait()
.map_err(std::io::Error::from)?;
}
} else if ep.attributes == EndpointAttributes::Interrupt as u8 {
} else if ep.transfer_type() == Some(EndpointAttributes::Interrupt) {
// interrupt
// Release Mutex before blocking so other URBs on this interface aren't starved.
if let Direction::In = ep.direction() {
@ -359,7 +359,7 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
let completion = endpoint.transfer_blocking(buffer, timeout);
completion.status.map_err(std::io::Error::from)?;
}
} else if ep.attributes == EndpointAttributes::Bulk as u8 {
} else if ep.transfer_type() == Some(EndpointAttributes::Bulk) {
// bulk
if let Direction::In = ep.direction() {
// bulk in - round up to max_packet_size as required by USB spec
@ -387,7 +387,7 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
let completion = endpoint.transfer_blocking(buffer, timeout);
completion.status.map_err(std::io::Error::from)?;
}
} else if ep.attributes == EndpointAttributes::Isochronous as u8 {
} else if ep.transfer_type() == Some(EndpointAttributes::Isochronous) {
// Isochronous transfer
if request.iso_packet_descriptors.is_empty() {
warn!("ISO transfer on ep {:02x} but no packet descriptors", ep.address);

View file

@ -25,6 +25,7 @@ mod consts;
mod device;
mod endpoint;
pub mod hid;
pub mod uac;
mod host;
mod interface;
mod setup;

608
lib/src/uac.rs Normal file
View file

@ -0,0 +1,608 @@
//! USB Audio Class 1 (UAC1) loopback device for testing isochronous transfers.
use super::*;
/// Capacity in bytes: 5 frames of 48kHz 16-bit stereo (5 * 192 = 960).
const LOOPBACK_BUFFER_CAPACITY: usize = 960;
/// Shared ring buffer connecting playback OUT to capture IN.
#[derive(Debug)]
pub struct UacLoopbackBuffer {
inner: Mutex<VecDeque<u8>>,
}
impl UacLoopbackBuffer {
pub fn new() -> Self {
Self {
inner: Mutex::new(VecDeque::with_capacity(LOOPBACK_BUFFER_CAPACITY)),
}
}
/// Write audio data into the buffer. Drops oldest samples if over capacity.
pub fn write(&self, data: &[u8]) {
let mut buf = self.inner.lock().unwrap_or_else(|e| e.into_inner());
buf.extend(data);
let excess = buf.len().saturating_sub(LOOPBACK_BUFFER_CAPACITY);
if excess > 0 {
buf.drain(..excess);
}
}
/// Read up to `len` bytes from the buffer. Pads with silence on underrun.
pub fn read(&self, len: usize) -> Vec<u8> {
let mut buf = self.inner.lock().unwrap_or_else(|e| e.into_inner());
let available = buf.len().min(len);
let mut result: Vec<u8> = buf.drain(..available).collect();
result.resize(len, 0); // pad with silence
result
}
/// Clear all buffered data.
pub fn clear(&self) {
self.inner.lock().unwrap_or_else(|e| e.into_inner()).clear();
}
}
/// Handler for the AudioControl interface (interface 0).
/// STALLs all class-specific requests — no Feature Unit in this topology.
#[derive(Debug)]
pub struct UacControlHandler;
impl UacControlHandler {
pub fn new() -> Self {
Self
}
}
impl UsbInterfaceHandler for UacControlHandler {
fn handle_urb(
&self,
_interface: &UsbInterface,
request: UrbRequest,
) -> Result<UrbResponse> {
// STALL all class-specific control requests (no Feature Unit)
if request.ep.is_ep0() {
return Ok(UrbResponse { status: -32, ..Default::default() }); // EPIPE = STALL
}
Ok(UrbResponse::default())
}
fn get_class_specific_descriptor(&self) -> Vec<u8> {
vec![
// --- AC Interface Header (UAC1 spec Table 4-2) ---
0x0A, // bLength: 10
0x24, // bDescriptorType: CS_INTERFACE
0x01, // bDescriptorSubtype: HEADER
0x00, 0x01, // bcdADC: 1.00
0x34, 0x00, // wTotalLength: 52 (10 + 12 + 9 + 12 + 9)
0x02, // bInCollection: 2 streaming interfaces
0x01, // baInterfaceNr(1): interface 1
0x02, // baInterfaceNr(2): interface 2
// --- Input Terminal 1: USB Streaming (playback source) ---
0x0C, // bLength: 12
0x24, // bDescriptorType: CS_INTERFACE
0x02, // bDescriptorSubtype: INPUT_TERMINAL
0x01, // bTerminalID: 1
0x01, 0x01, // wTerminalType: USB Streaming (0x0101)
0x00, // bAssocTerminal: 0
0x02, // bNrChannels: 2 (stereo)
0x03, 0x00, // wChannelConfig: left + right
0x00, // iChannelNames: 0
0x00, // iTerminal: 0
// --- Output Terminal 2: Speaker (playback sink) ---
0x09, // bLength: 9
0x24, // bDescriptorType: CS_INTERFACE
0x03, // bDescriptorSubtype: OUTPUT_TERMINAL
0x02, // bTerminalID: 2
0x01, 0x03, // wTerminalType: Speaker (0x0301)
0x00, // bAssocTerminal: 0
0x01, // bSourceID: 1 (Input Terminal 1)
0x00, // iTerminal: 0
// --- Input Terminal 3: Microphone (capture source) ---
0x0C, // bLength: 12
0x24, // bDescriptorType: CS_INTERFACE
0x02, // bDescriptorSubtype: INPUT_TERMINAL
0x03, // bTerminalID: 3
0x01, 0x02, // wTerminalType: Microphone (0x0201)
0x00, // bAssocTerminal: 0
0x02, // bNrChannels: 2 (stereo)
0x03, 0x00, // wChannelConfig: left + right
0x00, // iChannelNames: 0
0x00, // iTerminal: 0
// --- Output Terminal 4: USB Streaming (capture sink) ---
0x09, // bLength: 9
0x24, // bDescriptorType: CS_INTERFACE
0x03, // bDescriptorSubtype: OUTPUT_TERMINAL
0x04, // bTerminalID: 4
0x01, 0x01, // wTerminalType: USB Streaming (0x0101)
0x00, // bAssocTerminal: 0
0x03, // bSourceID: 3 (Input Terminal 3)
0x00, // iTerminal: 0
]
}
fn as_any(&self) -> &dyn Any {
self
}
}
/// Handler for AudioStreaming OUT interface (playback).
/// Writes received audio data into the shared loopback buffer.
#[derive(Debug)]
pub struct UacStreamOutHandler {
buffer: Arc<UacLoopbackBuffer>,
}
impl UacStreamOutHandler {
pub fn new(buffer: Arc<UacLoopbackBuffer>) -> Self {
Self { buffer }
}
}
impl UsbInterfaceHandler for UacStreamOutHandler {
fn handle_urb(
&self,
_interface: &UsbInterface,
request: UrbRequest,
) -> Result<UrbResponse> {
if request.ep.is_ep0() {
return Ok(UrbResponse { status: -32, ..Default::default() });
}
// ISO OUT: write audio data into loopback buffer
if request.ep.transfer_type() == Some(EndpointAttributes::Isochronous) {
let descriptors: Vec<IsoPacketDescriptor> = request
.iso_packet_descriptors
.iter()
.map(|d| {
let offset = d.offset as usize;
let length = d.length as usize;
if offset + length <= request.data.len() {
self.buffer.write(&request.data[offset..offset + length]);
}
IsoPacketDescriptor {
offset: d.offset,
length: d.length,
actual_length: d.length,
status: 0,
}
})
.collect();
return Ok(UrbResponse {
data: vec![],
iso_packet_descriptors: descriptors,
status: 0,
start_frame: request.start_frame,
error_count: 0,
});
}
Ok(UrbResponse::default())
}
fn get_class_specific_descriptor(&self) -> Vec<u8> {
vec![
// --- AS General Descriptor (UAC1 spec Table 4-19) ---
0x07, // bLength: 7
0x24, // bDescriptorType: CS_INTERFACE
0x01, // bDescriptorSubtype: AS_GENERAL
0x01, // bTerminalLink: 1 (Input Terminal 1 — playback)
0x01, // bDelay: 1 frame
0x01, 0x00, // wFormatTag: PCM (0x0001)
// --- Format Type I Descriptor (UAC1 spec Table 2-1, Format Type I) ---
0x0B, // bLength: 11
0x24, // bDescriptorType: CS_INTERFACE
0x02, // bDescriptorSubtype: FORMAT_TYPE
0x01, // bFormatType: FORMAT_TYPE_I
0x02, // bNrChannels: 2 (stereo)
0x02, // bSubframeSize: 2 bytes (16-bit)
0x10, // bBitResolution: 16
0x01, // bSamFreqType: 1 (one discrete frequency)
0x80, 0xBB, 0x00, // tSamFreq: 48000 Hz (0x00BB80, little-endian)
]
}
fn set_alt_setting(&self, _alt: u8) -> Result<()> {
self.buffer.clear();
Ok(())
}
fn as_any(&self) -> &dyn Any {
self
}
}
/// Handler for AudioStreaming IN interface (capture).
/// Reads audio data from the shared loopback buffer; returns silence on underrun.
#[derive(Debug)]
pub struct UacStreamInHandler {
buffer: Arc<UacLoopbackBuffer>,
}
impl UacStreamInHandler {
pub fn new(buffer: Arc<UacLoopbackBuffer>) -> Self {
Self { buffer }
}
}
impl UsbInterfaceHandler for UacStreamInHandler {
fn handle_urb(
&self,
_interface: &UsbInterface,
request: UrbRequest,
) -> Result<UrbResponse> {
if request.ep.is_ep0() {
return Ok(UrbResponse { status: -32, ..Default::default() });
}
// ISO IN: read audio data from loopback buffer
if request.ep.transfer_type() == Some(EndpointAttributes::Isochronous) {
let mut data = Vec::new();
let descriptors: Vec<IsoPacketDescriptor> = request
.iso_packet_descriptors
.iter()
.map(|d| {
let packet_data = self.buffer.read(d.length as usize);
data.extend_from_slice(&packet_data);
IsoPacketDescriptor {
offset: d.offset,
length: d.length,
actual_length: d.length,
status: 0,
}
})
.collect();
return Ok(UrbResponse {
data,
iso_packet_descriptors: descriptors,
status: 0,
start_frame: request.start_frame,
error_count: 0,
});
}
Ok(UrbResponse::default())
}
fn get_class_specific_descriptor(&self) -> Vec<u8> {
vec![
// --- AS General Descriptor ---
0x07, // bLength: 7
0x24, // bDescriptorType: CS_INTERFACE
0x01, // bDescriptorSubtype: AS_GENERAL
0x04, // bTerminalLink: 4 (Output Terminal 4 — capture)
0x01, // bDelay: 1 frame
0x01, 0x00, // wFormatTag: PCM (0x0001)
// --- Format Type I Descriptor ---
0x0B, // bLength: 11
0x24, // bDescriptorType: CS_INTERFACE
0x02, // bDescriptorSubtype: FORMAT_TYPE
0x01, // bFormatType: FORMAT_TYPE_I
0x02, // bNrChannels: 2 (stereo)
0x02, // bSubframeSize: 2 bytes (16-bit)
0x10, // bBitResolution: 16
0x01, // bSamFreqType: 1 (one discrete frequency)
0x80, 0xBB, 0x00, // tSamFreq: 48000 Hz
]
}
fn as_any(&self) -> &dyn Any {
self
}
}
/// Build a complete UAC1 loopback device with AudioControl + two AudioStreaming interfaces.
pub fn build_uac_loopback_device() -> std::io::Result<UsbDevice> {
let buffer = Arc::new(UacLoopbackBuffer::new());
let control_handler: Arc<dyn UsbInterfaceHandler> = Arc::new(UacControlHandler::new());
let out_handler: Arc<dyn UsbInterfaceHandler> = Arc::new(UacStreamOutHandler::new(buffer.clone()));
let in_handler: Arc<dyn UsbInterfaceHandler> = Arc::new(UacStreamInHandler::new(buffer));
let mut device = UsbDevice::new(0)?;
device.vendor_id = 0x1234;
device.product_id = 0x5678;
device.device_class = 0x00; // per-interface
device.speed = UsbSpeed::Full as u32;
device.usb_version = device::Version { major: 2, minor: 0, patch: 0 };
device.device_bcd = device::Version { major: 1, minor: 0, patch: 0 };
device.string_manufacturer = device.new_string("Test")?;
device.string_product = device.new_string("UAC1 Loopback")?;
// Interface 0: AudioControl (single alt setting, no endpoints)
let ac_class_desc = control_handler.get_class_specific_descriptor();
device.interface_states.push(InterfaceState::new(UsbInterface {
interface_class: ClassCode::Audio as u8,
interface_subclass: 0x01, // AudioControl
interface_protocol: 0x00,
endpoints: vec![],
string_interface: 0,
class_specific_descriptor: ac_class_desc,
handler: control_handler,
}));
// Interface 1: AudioStreaming OUT (playback) — alt 0 (zero-bw) + alt 1 (active)
let out_class_desc = out_handler.get_class_specific_descriptor();
let out_alt0 = UsbInterface {
interface_class: ClassCode::Audio as u8,
interface_subclass: 0x02, // AudioStreaming
interface_protocol: 0x00,
endpoints: vec![], // zero-bandwidth
string_interface: 0,
class_specific_descriptor: vec![],
handler: out_handler.clone(),
};
let out_alt1 = UsbInterface {
interface_class: ClassCode::Audio as u8,
interface_subclass: 0x02,
interface_protocol: 0x00,
endpoints: vec![UsbEndpoint {
address: 0x01, // OUT
attributes: 0x0D, // isochronous adaptive
max_packet_size: 192,
interval: 1,
}],
string_interface: 0,
class_specific_descriptor: out_class_desc,
handler: out_handler,
};
device.interface_states.push(InterfaceState::with_alt_settings(vec![out_alt0, out_alt1]));
// Interface 2: AudioStreaming IN (capture) — alt 0 (zero-bw) + alt 1 (active)
let in_class_desc = in_handler.get_class_specific_descriptor();
let in_alt0 = UsbInterface {
interface_class: ClassCode::Audio as u8,
interface_subclass: 0x02,
interface_protocol: 0x00,
endpoints: vec![],
string_interface: 0,
class_specific_descriptor: vec![],
handler: in_handler.clone(),
};
let in_alt1 = UsbInterface {
interface_class: ClassCode::Audio as u8,
interface_subclass: 0x02,
interface_protocol: 0x00,
endpoints: vec![UsbEndpoint {
address: 0x82, // IN
attributes: 0x05, // isochronous async
max_packet_size: 192,
interval: 1,
}],
string_interface: 0,
class_specific_descriptor: in_class_desc,
handler: in_handler,
};
device.interface_states.push(InterfaceState::with_alt_settings(vec![in_alt0, in_alt1]));
Ok(device)
}
#[cfg(test)]
mod tests {
use crate::util::tests::*;
use super::*;
#[test]
fn loopback_buffer_write_read() {
let buf = UacLoopbackBuffer::new();
buf.write(&[1, 2, 3, 4]);
assert_eq!(buf.read(4), vec![1, 2, 3, 4]);
// Buffer should be empty now
assert_eq!(buf.read(2), vec![0, 0]);
}
#[test]
fn loopback_buffer_underrun() {
let buf = UacLoopbackBuffer::new();
let result = buf.read(4);
assert_eq!(result, vec![0, 0, 0, 0]);
}
#[test]
fn loopback_buffer_overflow() {
let buf = UacLoopbackBuffer::new();
// Write more than capacity: 100 old bytes + CAPACITY new bytes
let mut big_data = vec![0xBB; 100];
big_data.extend(vec![0xAA; LOOPBACK_BUFFER_CAPACITY]);
buf.write(&big_data);
// Should have dropped the oldest 100 bytes (0xBB), kept newest CAPACITY (0xAA)
let result = buf.read(LOOPBACK_BUFFER_CAPACITY);
assert_eq!(result.len(), LOOPBACK_BUFFER_CAPACITY);
assert!(result.iter().all(|&b| b == 0xAA), "oldest bytes should have been dropped");
// Buffer is now empty
assert_eq!(buf.read(1), vec![0]);
}
#[test]
fn desc_verify_control() {
setup_test_logger();
let handler = UacControlHandler::new();
verify_descriptor(&handler.get_class_specific_descriptor());
}
#[test]
fn control_handler_stall() {
setup_test_logger();
let handler = UacControlHandler::new();
let interface = UsbInterface {
interface_class: ClassCode::Audio as u8,
interface_subclass: 0x01,
interface_protocol: 0x00,
endpoints: vec![],
string_interface: 0,
class_specific_descriptor: handler.get_class_specific_descriptor(),
handler: Arc::new(UacControlHandler::new()),
};
// Send an unknown class request (SET_CUR to some unit)
let request = UrbRequest {
ep: UsbEndpoint {
address: 0x00,
attributes: EndpointAttributes::Control as u8,
max_packet_size: 64,
interval: 0,
},
setup: SetupPacket {
request_type: 0x21, // class, interface, host-to-device
request: 0x01, // SET_CUR
value: 0x0100,
index: 0x0000,
length: 0,
},
..Default::default()
};
let response = handler.handle_urb(&interface, request).unwrap();
assert_eq!(response.status, -32); // EPIPE = STALL
}
#[test]
fn desc_verify_stream_out() {
setup_test_logger();
let handler = UacStreamOutHandler::new(Arc::new(UacLoopbackBuffer::new()));
verify_descriptor(&handler.get_class_specific_descriptor());
}
#[test]
fn stream_out_handler_iso() {
setup_test_logger();
let buffer = Arc::new(UacLoopbackBuffer::new());
let handler = UacStreamOutHandler::new(buffer.clone());
let interface = UsbInterface {
interface_class: ClassCode::Audio as u8,
interface_subclass: 0x02,
interface_protocol: 0x00,
endpoints: vec![UsbEndpoint {
address: 0x01,
attributes: 0x0D, // isochronous adaptive
max_packet_size: 192,
interval: 1,
}],
string_interface: 0,
class_specific_descriptor: handler.get_class_specific_descriptor(),
handler: Arc::new(UacStreamOutHandler::new(Arc::new(UacLoopbackBuffer::new()))),
};
let audio_data: Vec<u8> = (0..192).map(|i| (i & 0xFF) as u8).collect();
let request = UrbRequest {
ep: UsbEndpoint {
address: 0x01,
attributes: 0x0D,
max_packet_size: 192,
interval: 1,
},
data: audio_data.clone(),
number_of_packets: 1,
iso_packet_descriptors: vec![IsoPacketDescriptor {
offset: 0,
length: 192,
actual_length: 0,
status: 0,
}],
..Default::default()
};
let response = handler.handle_urb(&interface, request).unwrap();
assert_eq!(response.status, 0);
assert_eq!(response.iso_packet_descriptors.len(), 1);
assert_eq!(response.iso_packet_descriptors[0].actual_length, 192);
// Verify data was written to loopback buffer
assert_eq!(buffer.read(192), audio_data);
}
#[test]
fn desc_verify_stream_in() {
setup_test_logger();
let handler = UacStreamInHandler::new(Arc::new(UacLoopbackBuffer::new()));
verify_descriptor(&handler.get_class_specific_descriptor());
}
#[test]
fn stream_in_handler_iso() {
setup_test_logger();
let buffer = Arc::new(UacLoopbackBuffer::new());
let handler = UacStreamInHandler::new(buffer.clone());
let interface = UsbInterface {
interface_class: ClassCode::Audio as u8,
interface_subclass: 0x02,
interface_protocol: 0x00,
endpoints: vec![UsbEndpoint {
address: 0x82,
attributes: 0x05, // isochronous async
max_packet_size: 192,
interval: 1,
}],
string_interface: 0,
class_specific_descriptor: handler.get_class_specific_descriptor(),
handler: Arc::new(UacStreamInHandler::new(Arc::new(UacLoopbackBuffer::new()))),
};
// Pre-fill buffer with known data
let audio_data: Vec<u8> = (0..192).map(|i| (i & 0xFF) as u8).collect();
buffer.write(&audio_data);
let request = UrbRequest {
ep: UsbEndpoint {
address: 0x82,
attributes: 0x05,
max_packet_size: 192,
interval: 1,
},
transfer_buffer_length: 192,
number_of_packets: 1,
iso_packet_descriptors: vec![IsoPacketDescriptor {
offset: 0,
length: 192,
actual_length: 0,
status: 0,
}],
..Default::default()
};
let response = handler.handle_urb(&interface, request).unwrap();
assert_eq!(response.status, 0);
assert_eq!(response.data, audio_data);
assert_eq!(response.iso_packet_descriptors.len(), 1);
assert_eq!(response.iso_packet_descriptors[0].actual_length, 192);
}
#[test]
fn stream_in_handler_silence() {
setup_test_logger();
let buffer = Arc::new(UacLoopbackBuffer::new());
let handler = UacStreamInHandler::new(buffer);
let interface = UsbInterface {
interface_class: ClassCode::Audio as u8,
interface_subclass: 0x02,
interface_protocol: 0x00,
endpoints: vec![],
string_interface: 0,
class_specific_descriptor: handler.get_class_specific_descriptor(),
handler: Arc::new(UacStreamInHandler::new(Arc::new(UacLoopbackBuffer::new()))),
};
let request = UrbRequest {
ep: UsbEndpoint {
address: 0x82,
attributes: 0x05,
max_packet_size: 192,
interval: 1,
},
transfer_buffer_length: 192,
number_of_packets: 1,
iso_packet_descriptors: vec![IsoPacketDescriptor {
offset: 0,
length: 192,
actual_length: 0,
status: 0,
}],
..Default::default()
};
let response = handler.handle_urb(&interface, request).unwrap();
assert_eq!(response.status, 0);
// All silence (zeros) because buffer is empty
assert_eq!(response.data, vec![0u8; 192]);
}
}

View file

@ -120,7 +120,7 @@ pub(crate) mod tests {
) -> std::io::Result<UrbResponse> {
let ep = request.ep;
if ep.attributes == EndpointAttributes::Isochronous as u8 {
if ep.transfer_type() == Some(EndpointAttributes::Isochronous) {
if let crate::Direction::In = ep.direction() {
// ISO IN: generate test data
let mut data = Vec::new();