Compare commits

...

6 Commits

Author SHA1 Message Date
ktims 2231c8db9c badges 2026-05-17 23:32:05 -07:00
ktims 610ec6597d exclude examples from package 2026-05-17 23:14:53 -07:00
ktims 98e0fb0927 add license 2026-05-17 23:11:32 -07:00
ktims a848e236d4 add pre-commit 2026-05-17 22:58:14 -07:00
ktims 008c62318e examples improvements 2026-05-17 22:58:00 -07:00
ktims 2b39987ef5 readme, doc comments, clippies 2026-05-17 22:53:33 -07:00
13 changed files with 190 additions and 48 deletions
+2
View File
@@ -1 +1,3 @@
/target /target
.zed
.gdbinit
+27
View File
@@ -0,0 +1,27 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: fix-byte-order-marker
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-yaml
- id: end-of-file-fixer
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit
rev: v3.6.2
hooks:
- id: validate_manifest
- repo: https://github.com/jorisroovers/gitlint
rev: v0.19.1
hooks:
- id: gitlint
- repo: https://github.com/FeryET/pre-commit-rust
rev: v1.2.1
hooks:
- id: fmt
- id: cargo-check
- id: clippy
- id: build
+12
View File
@@ -12,10 +12,16 @@ usbd-uac2 = { path = "." }
[package] [package]
name = "usbd-uac2" name = "usbd-uac2"
description = "USB Audio Class 2.0 for usb-device" description = "USB Audio Class 2.0 for usb-device"
readme = "README.md"
authors = ["Keenan Tims <ktims@gotroot.ca>"] authors = ["Keenan Tims <ktims@gotroot.ca>"]
repository = "https://github.com/ktims/usbd-uac2"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
license = "MIT"
keywords = ["no-std", "usb-device", "audio"] keywords = ["no-std", "usb-device", "audio"]
exclude = [
"examples/**"
]
[features] [features]
defmt = ["dep:defmt", "usb-device/defmt"] defmt = ["dep:defmt", "usb-device/defmt"]
@@ -27,3 +33,9 @@ embedded-io = "0.7.1"
modular-bitfield = "0.13.1" modular-bitfield = "0.13.1"
num-traits = { version = "0.2.19", default-features = false } num-traits = { version = "0.2.19", default-features = false }
usb-device.workspace = true usb-device.workspace = true
[profile.release]
opt-level = "z"
lto = true
debug = true
codegen-units = 1
+18
View File
@@ -0,0 +1,18 @@
Copyright 2026 Keenan Tims
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+47
View File
@@ -0,0 +1,47 @@
![Crates.io Version](https://img.shields.io/crates/v/usbd-uac2) ![docs.rs](https://img.shields.io/docsrs/usbd-uac2)
# usbd-uac2
USB Audio Class 2.0
This crate provides a USB Audio Class 2.0 implementation for
[usb-device](https://crates.io/crates/usb-device). It implements all
required elements of the specification, however many controls are not
implemented (e.g. mixers, effects).
Device behaviour is driven by implementing the `ClockSource` and
`AudioHandler` traits to configure the audio pipeline and source/sink data
to/from USB.
Example (creates a UAC2 device with in and out streams):
```rust
let mut audio = YourTraitImpl {...};
let config = UsbAudioClassConfig::new(usb_speed, FunctionCode::IoBox, &mut audio)
// base_id is USB entity ID, id 1 is always taken by the clock source, and each stream builds 2 entities
.with_output_config(TerminalConfig::builder().base_id(2).build())
.with_input_config(TerminalConfig::builder().base_id(4).build());
let mut uac2 = config.build(&usb_bus).unwrap();
let mut usb_dev = usbd_uac2::builder(&usb_bus, UsbVidPid(0x1209, 0x0001))
.strings(&[StringDescriptors::default()
.manufacturer("usbd_uac2")
.product("example")])
.unwrap()
.max_packet_size_0(64) // Required to be 64 on HS
.unwrap()
.build();
```
No work needs to be done in the poll loop, the class implementation will
call your trait callbacks as required, just call the usb poll as usual:
```rust
loop {
usb_dev.poll(&mut [&mut uac2]);
}
```
See the trait documentation or examples for additional details.
+1 -1
View File
@@ -3,7 +3,7 @@ Two example backends are provided, both based on the LPCXpresso55S28 demo board,
## LPC55S28 (LPCXpresso55S28) ## LPC55S28 (LPCXpresso55S28)
These examples work on the These examples work on the
(LPCXpresso55S28)[https://www.nxp.com/design/design-center/software/development-software/mcuxpresso-software-and-tools-/lpcxpresso-boards/lpcxpresso55s28-development-board:LPC55S28-EVK] [LPCXpresso55S28](https://www.nxp.com/design/design-center/software/development-software/mcuxpresso-software-and-tools-/lpcxpresso-boards/lpcxpresso55s28-development-board:LPC55S28-EVK)
development board. They use the onboard WM8904 codec, so don't require any development board. They use the onboard WM8904 codec, so don't require any
additional hardware for audio I/O. additional hardware for audio I/O.
@@ -1,10 +1,3 @@
[target.thumbv8m.main-none-eabi]
rustflags = [
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
"-C", "debug-assertions",
]
[target.thumbv8m.main-none-eabihf] [target.thumbv8m.main-none-eabihf]
rustflags = [ rustflags = [
"-C", "link-arg=-Tlink.x", "-C", "link-arg=-Tlink.x",
-6
View File
@@ -24,9 +24,3 @@ static_cell = "2.1.1"
lpc55-hal = { git = "https://github.com/ktims/lpc55-hal", branch = "main" } lpc55-hal = { git = "https://github.com/ktims/lpc55-hal", branch = "main" }
usb-device.workspace = true usb-device.workspace = true
usbd-uac2 = { workspace = true, features = ["defmt"] } usbd-uac2 = { workspace = true, features = ["defmt"] }
[profile.release]
opt-level = "z"
lto = true
debug = true
codegen-units = 1
+17 -11
View File
@@ -38,7 +38,7 @@ use pac::interrupt;
use static_cell::StaticCell; use static_cell::StaticCell;
use usb_device::{ use usb_device::{
bus::{self}, bus::{self},
device::{StringDescriptors, UsbDeviceBuilder, UsbVidPid}, device::{StringDescriptors, UsbVidPid},
}; };
use usbd_uac2::TerminalConfig; use usbd_uac2::TerminalConfig;
use usbd_uac2::{ use usbd_uac2::{
@@ -53,6 +53,12 @@ mod dma;
mod hw; mod hw;
mod wm8904; mod wm8904;
// pid.codes test IDs
const USB_VID: u16 = 0x1209;
const USB_PID: u16 = 0x0001;
const USB_MANUFACTURER: &str = "usbd_uac2";
const USB_PRODUCT: &str = "DMA example device";
const CODEC_I2C_ADDR: u8 = 0b0011010; const CODEC_I2C_ADDR: u8 = 0b0011010;
const MCLK_FREQ: u32 = 12288000; const MCLK_FREQ: u32 = 12288000;
@@ -199,6 +205,11 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize, B: bus::UsbBus> AudioHandler<'
self.running.store(true, Ordering::Relaxed) self.running.store(true, Ordering::Relaxed)
} }
} }
fn audio_data_tx(
&mut self,
_ep: &usb_device::endpoint::Endpoint<'_, B, usb_device::endpoint::In>,
) {
}
/// Provide rate feedback to the host. P-only is stable and works fine, with /// Provide rate feedback to the host. P-only is stable and works fine, with
/// most hosts. The host can either filter it internally or treat it /// most hosts. The host can either filter it internally or treat it
@@ -229,7 +240,7 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize, B: bus::UsbBus> AudioHandler<'
// 0.2% which is a huge clock error // 0.2% which is a huge clock error
let max_allowed_deviation = nominal_v / 500; let max_allowed_deviation = nominal_v / 500;
let p_term = -(error_permille * nominal_v) / 256000; // this works reasonably well to keep the buffer let p_term = -(error_permille * nominal_v) / 256000; // this works reasonably well to keep the buffer
let i_term = 0; // placeholder let i_term = 0; // placeholder
let mut v = nominal_v + p_term + i_term; let mut v = nominal_v + p_term + i_term;
@@ -379,18 +390,13 @@ fn main() -> ! {
.with_output_config(TerminalConfig::builder().base_id(2).build()); .with_output_config(TerminalConfig::builder().base_id(2).build());
let mut uac2 = config.build(&usb_bus).unwrap(); let mut uac2 = config.build(&usb_bus).unwrap();
let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x1209, 0xcc1d)) let mut usb_dev = usbd_uac2::builder(&usb_bus, UsbVidPid(USB_VID, USB_PID))
.composite_with_iads()
.strings(&[StringDescriptors::default() .strings(&[StringDescriptors::default()
.manufacturer("Generic") .manufacturer(USB_MANUFACTURER)
.product("usbd_uac2 device") .product(USB_PRODUCT)])
.serial_number("123456789")])
.unwrap() .unwrap()
.max_packet_size_0(64) .max_packet_size_0(64) // Required to be 64 on HS, allowed on FS
.unwrap() .unwrap()
.device_class(0xef)
.device_sub_class(0x02)
.device_protocol(0x01)
.build(); .build();
defmt::info!("main loop"); defmt::info!("main loop");
-7
View File
@@ -1,10 +1,3 @@
[target.thumbv8m.main-none-eabi]
rustflags = [
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
"-C", "debug-assertions",
]
[target.thumbv8m.main-none-eabihf] [target.thumbv8m.main-none-eabihf]
rustflags = [ rustflags = [
"-C", "link-arg=-Tlink.x", "-C", "link-arg=-Tlink.x",
-6
View File
@@ -23,9 +23,3 @@ panic-probe = { version = "1.0.0", features = ["print-defmt"] }
lpc55-hal = { git = "https://github.com/ktims/lpc55-hal", branch = "main" } lpc55-hal = { git = "https://github.com/ktims/lpc55-hal", branch = "main" }
usb-device.workspace = true usb-device.workspace = true
usbd-uac2 = { workspace = true, features = ["defmt"] } usbd-uac2 = { workspace = true, features = ["defmt"] }
[profile.release]
opt-level = "z"
lto = true
debug = true
codegen-units = 1
+50 -5
View File
@@ -1,3 +1,47 @@
//! USB Audio Class 2.0
//!
//! This crate provides a USB Audio Class 2.0 implementation for
//! [usb-device](https://crates.io/crates/usb-device). It implements all
//! required elements of the specification, however many controls are not
//! implemented (e.g. mixers, effects).
//!
//! Device behaviour is driven by implementing the `ClockSource` and
//! `AudioHandler` traits to configure the audio pipeline and source/sink data
//! to/from USB.
//!
//! Example (creates a UAC2 device with in and out streams):
//!
//! ```ignore
//! let mut audio = YourTraitImpl {...};
//!
//! let config = UsbAudioClassConfig::new(usb_speed, FunctionCode::IoBox, &mut audio)
//! // base_id is USB entity ID, id 1 is always taken by the clock source, and each stream builds 2 entities
//! .with_output_config(TerminalConfig::builder().base_id(2).build())
//! .with_input_config(TerminalConfig::builder().base_id(4).build());
//!
//! let mut uac2 = config.build(&usb_bus).unwrap();
//!
//! let mut usb_dev = usbd_uac2::builder(&usb_bus, UsbVidPid(0x1209, 0x0001))
//! .strings(&[StringDescriptors::default()
//! .manufacturer("usbd_uac2")
//! .product("example")])
//! .unwrap()
//! .max_packet_size_0(64) // Required to be 64 on HS
//! .unwrap()
//! .build();
//! ```
//!
//! No work needs to be done in the poll loop, the class implementation will
//! call your trait callbacks as required, just call the usb poll as usual:
//!
//! ```ignore
//! loop {
//! usb_dev.poll(&mut [&mut uac2]);
//! }
//! ```
//!
//! See the trait documentation or examples for additional details.
#![no_std] #![no_std]
#![allow(dead_code)] #![allow(dead_code)]
@@ -9,11 +53,12 @@ mod log;
use core::cmp::Ordering; use core::cmp::Ordering;
use core::marker::PhantomData; use core::marker::PhantomData;
use byteorder_embedded_io::{LittleEndian, ReadBytesExt, WriteBytesExt};
use constants::*; use constants::*;
use descriptors::*; use descriptors::*;
#[allow(unused_imports)]
use log::*; use log::*;
use byteorder_embedded_io::{LittleEndian, ReadBytesExt, WriteBytesExt};
use num_traits::{ConstZero, ToPrimitive}; use num_traits::{ConstZero, ToPrimitive};
use usb_device::control::{Recipient, Request, RequestType}; use usb_device::control::{Recipient, Request, RequestType};
use usb_device::device::{DEFAULT_ALTERNATE_SETTING, UsbDeviceBuilder, UsbVidPid}; use usb_device::device::{DEFAULT_ALTERNATE_SETTING, UsbDeviceBuilder, UsbVidPid};
@@ -936,8 +981,8 @@ impl<'a, B: UsbBus, AU: AudioHandler<'a, B> + ClockSource> UsbClass<B>
break; break;
} }
Err(UsbError::WouldBlock) => break, Err(UsbError::WouldBlock) => break,
Err(err) => { Err(_err) => {
debug!("EP OUT error {:?}", err); debug!("EP OUT error {:?}", _err);
} }
} }
} }
@@ -962,8 +1007,8 @@ impl<'a, B: UsbBus, AU: AudioHandler<'a, B> + ClockSource> UsbAudioClass<'a, B,
UsbSpeed::Low | UsbSpeed::Full => fb_ep.write(&fb.to_bytes_10_14()), UsbSpeed::Low | UsbSpeed::Full => fb_ep.write(&fb.to_bytes_10_14()),
UsbSpeed::High | UsbSpeed::Super => fb_ep.write(&fb.to_bytes_12_13()), UsbSpeed::High | UsbSpeed::Super => fb_ep.write(&fb.to_bytes_12_13()),
}; };
if let Err(e) = r { if let Err(_e) = r {
warn!(" feedback IN failed {:?}", e); warn!(" feedback IN failed {:?}", _e);
} }
} else { } else {
debug!(" feedback callback returned None"); debug!(" feedback callback returned None");
+16 -5
View File
@@ -7,25 +7,36 @@ pub use defmt::{debug, error, info, trace, warn};
mod no_defmt { mod no_defmt {
#[macro_export] #[macro_export]
macro_rules! trace { macro_rules! trace {
($($t:tt)*) => {}; ($($t:tt)*) => {
()
};
} }
#[macro_export] #[macro_export]
macro_rules! debug { macro_rules! debug {
($($t:tt)*) => {}; ($($t:tt)*) => {
()
};
} }
#[macro_export] #[macro_export]
macro_rules! info { macro_rules! info {
($($t:tt)*) => {}; ($($t:tt)*) => {
()
};
} }
#[macro_export] #[macro_export]
macro_rules! warn { macro_rules! warn {
($($t:tt)*) => {}; ($($t:tt)*) => {
()
};
} }
#[macro_export] #[macro_export]
macro_rules! error { macro_rules! error {
($($t:tt)*) => {}; ($($t:tt)*) => {
()
};
} }
} }
#[allow(unused_imports)]
#[cfg(not(feature = "defmt"))] #[cfg(not(feature = "defmt"))]
pub use no_defmt::*; pub use no_defmt::*;