blob: 0f7837a74661b96271b503bf395a1daf4c9cb35b [file] [view]
# Basic Audio Profile Unicast library
An implementation of the [Basic Audio Profile 1.0.2](spec).
It is assumed that a developer implementing this feature will have access to the BAP specification to reference specific tables and values not fully detailed in this document.
Note: For automated tools and AI agents, specialized skills are available in the repository (under `.agents/skills/`) to read and reference the specifications.
## Problem Statement
We are designing a mid-level library to simplify the establishment and management of Bluetooth LE Audio Unicast streams (BAP 1.0.2), which currently requires complex and repetitive interactions with PACS and ASCS state machines. The "Simple Control" feature aims to automate the mapping of varied peer capabilities to standard audio configurations and handle the stream setup state machine, providing a clean API that yields data stream handles to the client while retaining flexibility for testing unsupported configurations.
In our design, we strive to make this library easy to use for clients to connect and perform audio stream control operations.
This library uses the bt-ascs and bt-pacs client libraries, with the goal that
the client of this library should not need to use these libraries directly. In
some advanced situations, we may provide a way to use the ASCS and PACS clients
directly.
This library is focused on the Unicast roles defined in the spec: Unicast
Client and Unicast Server.
The current implementation status can be found in TODO.md.
## Basic design
The Specification defines a number of specific capabilities which must or may
be supported by the Unicast Server:
* Audio Capabilities (Table 7 & Table 13)
* QoS Configurations (Table 56)
It also defines a set of audio configurations for multiple channel audio in
section 4.4.
This version of the library provides simple control for audio streams. Fine-grained control is deferred to a later version; clients requiring fine-grained control should use the underlying libraries directly for now.
In either control method, once audio is started, the set of CIS along with
the codec configurations can be retrieved and provided to the client, so it
can be passed to an audio system which may need to send/receive audio.
### Simple Control
The simple control method is managed by a `BapUnicastManager` which can track multiple peers (identified by `PeerId`). It provides a high-level interface for clients to establish and control audio streams based on spec-defined configurations.
#### Initialization and Discovery
The manager is initialized without side effects. Calling the asynchronous `add_peer(PeerId)` triggers the connection and discovery of PACS and ASCS on the remote peer. This method returns the initial mapping of supported spec-defined **Configurations** to the set of valid **Codec Capability Settings** (e.g., `LC3_48_2`) that the peer supports, along with a stream of events for dynamic updates. If discovery fails, the method returns an error immediately.
#### Stream Control API
The manager provides the following asynchronous operations:
* **`start_streaming(PeerId, Configuration, CodecSetting)`**: Initiates the ASCS state machine to configure and enable the stream. It returns the established CIS objects (from `bt_gatt::connected_isochronous`) to the client, handing over ownership of the data plane.
* **`pause_streaming(PeerId, Option<ConnectedIsochronousStream>)`**: Moves the stream to a paused state (typically mapping to `QoS Configured` in ASCS). The client can indicate it expects to close the CIS to save power by returning the CIS object to the library. If omitted, the library attempts to keep the CIS connected to minimize resume latency.
* **`resume_streaming(PeerId)`**: Re-enables a paused stream. If the CIS was closed during pause, the library will re-establish it.
* **`stop_streaming(PeerId, ConnectedIsochronousStream)`**: Disables and releases the streams, returning the ASEs to the `Idle` state. The client must return the CIS object to the library so it can handle the teardown.
#### Peer Events
The manager provides a stream of events for each peer to notify the client of dynamic changes:
* **`CapabilitiesChanged`**: Emitted when the peer's available audio contexts or capabilities change (discovered via PACS notifications). It provides the updated mapping of supported configurations.
* **`StreamStateChanged`**: Emitted when a stream's state changes (e.g., due to peer-initiated transitions or autonomous release).
#### Design Principles
* **Prescriptive Execution**: The manager acts as a direct executor of the client's request. If a state transition fails (e.g., QoS rejected by the peer), the manager fails immediately and returns the error to the client rather than attempting to negotiate a fallback.
* **Trusting Validation**: While the manager returns a list of supported configurations, it does not strictly forbid the client from attempting to start a configuration not in that list. If the client provides a valid spec configuration, the manager will attempt to execute it.
* **State Tracking**: The manager tracks the active state of streams (which ASEs are in which state) to facilitate control operations, but does not update its stored capabilities list for a peer based on forced successful configurations.
```mermaid
sequenceDiagram
participant Client
participant Manager
participant Peer (ASCS/PACS)
Client->>Manager: add_peer(PeerId)
Manager->>Peer: Discover PACS & ASCS
Peer-->>Manager: Capabilities & ASEs
Manager-->>Client: Map<Configuration, Set<CodecSetting>>
Client->>Manager: start_streaming(PeerId, Config, Setting)
Manager->>Peer: Config Codec LTVs
Manager->>Peer: Config QoS
Manager->>Peer: Enable
Peer-->>Manager: CIS Established
Manager-->>Client: CIS Handles
```
### Fine grained control
Fine-grained control is deferred to a later version of this library. Clients requiring fine-grained control over endpoints and specific state transitions should use the `bt-ascs` and `bt-pacs` libraries directly.
## Error Handling
The library will define a centralized `Error` enum to represent failures in stream management and control operations.
### `src/error.rs` or `src/types.rs`
```rust
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// An error occurred in the underlying GATT interaction.
#[error("GATT error: {0}")]
Gatt(bt_gatt::Error),
/// An error occurred in the ASCS client.
#[error("ASCS error: {0}")]
Ascs(bt_ascs::Error),
/// The requested peer was not found in the manager.
#[error("Peer {0} not found")]
PeerNotFound(PeerId),
/// The operation requested is invalid in the current state.
#[error("Invalid state: {0}")]
InvalidState(String),
/// The configuration requested is not valid according to the spec.
#[error("Invalid configuration: {0}")]
InvalidConfiguration(String),
/// The peer rejected the operation (e.g. QoS or Codec configuration rejected).
#[error("Peer rejected operation: {0}")]
PeerRejected(String),
/// An internal error occurred.
#[error("Internal error: {0}")]
Internal(String),
}
```
### Usage Scenarios
* **`Gatt`**: Used when reading/writing characteristics fails during discovery or control point operations.
* **`Ascs`**: Used when the underlying `bt-ascs` library returns an error, e.g., failure to parse ASCS notifications.
* **`PeerNotFound`**: Returned by `start_streaming`, `pause_streaming`, etc., if the provided `PeerId` has not been added to the manager via `add_peer`.
* **`InvalidState`**: Returned if the client attempts to `resume_streaming` on a stream that is not paused, or `start_streaming` on a peer that is already streaming.
* **`InvalidConfiguration`**: Returned if the client requests a spec configuration that is known to be invalid or unsupported by the library's current implementation level.
* **`PeerRejected`**: Returned when the peer explicitly rejects a state transition in the ASCS control point (e.g., returning a failure response code for Config Codec or Config QoS).
## Files to be Created
To implement the Simple Control feature, we will create the following files in the `src/` directory:
#### `src/lib.rs`
* **Implementation**: The library root. It will declare the modules (`manager`, `client`, `types`, `mapping`) and re-export the public-facing API (like the `BapUnicastManager` and the configuration enums).
* **Rationale**: Standard Rust convention for a library crate. It defines the public surface area.
#### `src/manager.rs`
* **Implementation**: Contains the `BapUnicastManager` struct. It will handle the collection of peers, mapped by their `PeerId`. It will contain the `add_peer` function and delegate the actual stream control operations to the specific client instance for that peer.
* **Rationale**: Fulfills the requirement to have a manager that can track multiple peers to support future multi-device configurations, while keeping the top-level API clean.
#### `src/client.rs`
* **Implementation**: Contains a `BapUnicastClient` struct representing a connection to a *single* peer. This is where the core logic lives:
* Driving the ASCS state machine (Prescriptive execution).
* Interacting with `bt-pacs` and `bt-ascs`.
* Handling the "Trusting" validation where it attempts any valid configuration requested.
* Tracking the **Active State** of the streams on that specific peer.
* **Rationale**: Separates the "collection of peers" from the "single peer interaction", keeping the code modular and easier to maintain and test.
#### `src/types.rs`
* **Implementation**: Defines the strongly-typed enums and structs:
* An enum for spec-defined **Configurations** (e.g., `MonoAudioSource`, `Conversational`).
* An enum for **Codec Capability Settings** (e.g., `Lc3_48_2`).
* Structs to represent the valid pairs returned to the user.
* **Rationale**: Centralizes the definitions derived from the Bluetooth specification, ensuring type safety across the library.
#### `src/mapping.rs`
* **Implementation**: Contains the logic that looks at the raw capabilities returned by `bt-pacs` (frequencies, frame durations) and the endpoints available in `bt-ascs`, and calculates which of our strongly-typed spec configurations are actually possible.
* **Rationale**: This is the "main value" processing engine of the library. Keeping it in its own file prevents other files from becoming massive and hard to read.
#### `tests/tests.rs`
* **Implementation**: This will use the public API of the library to simulate full use cases. It will likely use mock implementations of the underlying Bluetooth services to simulate remote peers.
* **Rationale**: Ensures that all components work together correctly and that the public API behaves as expected without requiring real hardware.
## Integration with Dependencies
The library relies on `bt-pacs` and `bt-ascs` to interact with the remote peer's GATT services, and `bt-common` for shared types.
### `bt-common`
`bt-common` provides shared types and structures, including the LTV (Length-Type-Value) structures used for codec capabilities and configurations.
### `bt-pacs`
`bt-pacs` provides types for parsing Published Audio Capabilities (PAC) records and Audio Locations. The library reads these from the remote peer's PACS service.
Key types used from `bt-pacs`:
* **`PacRecord`**: Represents a capability record.
```rust
pub struct PacRecord {
pub codec_id: CodecId,
pub codec_specific_capabilities: Vec<CodecCapability>,
pub metadata: Vec<Metadata>,
}
```
`CodecCapability` (from `bt-common`) contains the supported parameters like sampling frequencies and frame durations.
### `bt-ascs`
The library uses `AudioStreamControlServiceClient` from `bt-ascs::client` to drive the ASCS state machine.
**API Contract for `AudioStreamControlServiceClient`:**
```rust
impl<T: bt_gatt::GattTypes> AudioStreamControlServiceClient<T> {
/// Discover the control point and all endpoints on the server.
pub async fn create(gatt_client: T::PeerService) -> Result<Self, Error>;
/// Configure the codec for a set of ASEs.
pub async fn configure_codec(
&mut self,
codec_configurations: Vec<CodecConfiguration>,
) -> Result<OperationResult<(AseId, QosParameters)>, Error>;
/// Configure QoS for a set of ASEs.
pub async fn configure_qos(
&mut self,
qos_configurations: Vec<QosConfiguration>,
) -> Result<OperationResult<AseId>, Error>;
/// Enable a set of ASEs.
pub async fn enable(
&mut self,
ases_with_metadata: Vec<AseIdWithMetadata>,
) -> Result<OperationResult<AseId>, Error>;
}
```
**Key Parameter Types:**
* **`CodecConfiguration`**:
```rust
pub struct CodecConfiguration {
pub ase_id: AseId,
pub target_latency: TargetLatency,
pub target_phy: TargetPhy,
pub codec_id: CodecId,
pub codec_specific_configuration: Vec<u8>, // LTV encoded
}
```
> [!NOTE]
> The LTV structures used for `codec_specific_configuration` are provided by the `bt-common` crate.
* **`QosConfiguration`**:
```rust
pub struct QosConfiguration {
pub ase_id: AseId,
pub cig_id: CigId,
pub cis_id: CisId,
pub sdu_interval: SduInterval,
pub framing: Framing,
pub phy: Vec<Phy>,
pub max_sdu: MaxSdu,
pub retransmission_number: u8,
pub max_transport_latency: MaxTransportLatency,
pub presentation_delay: PresentationDelay,
}
```
* **`AseIdWithMetadata`**:
```rust
pub struct AseIdWithMetadata {
pub ase_id: AseId,
pub metadata: Vec<Metadata>,
}
```
## Mapping Details
The library automates the mapping of raw peer capabilities and endpoints to standard BAP configurations. This logic resides in `src/mapping.rs`.
### Inputs
* **PACS Capabilities**: Discovered via `bt-pacs`, including supported sampling frequencies, frame durations, channel counts, and max supported codec frames per SDU.
* **ASCS Endpoints**: Discovered via `bt-ascs`, specifically the number of available Sink and Source ASEs (Audio Stream Endpoints).
### Outputs
* A mapping of strongly-typed **Configurations** (e.g., `Audio Configuration 1`) to a set of compatible **Codec Capability Settings** (e.g., `LC3_48_2`).
### Mapping Algorithm
For each standard BAP configuration defined in the specification (e.g., Section 4.4; see also Tables 7, 13, and 56 for specific capability and QoS parameters):
1. **Direction & ASE Check**: Verify the peer supports the required direction (Sink or Source) and has a sufficient number of ASEs available for that configuration.
* *Example*: `Audio Configuration 1` requires 1 Sink ASE.
2. **Codec Capability Intersection**: Intersect the peer's supported PAC parameters with the requirements of each `CodecSetting`.
* Check if the peer supports the required sampling frequency (e.g., 48 kHz).
* Check if the peer supports the required frame duration (e.g., 10 ms).
* Check if the peer supports the required channel count.
3. **Result**: If a configuration has at least one matching `CodecSetting` and sufficient ASEs, it is included in the returned map for the client to choose from.
## Alternatives / Future Plans
* **High-Level Intent API**: We considered exposing semantic intents (e.g., "Play Music") instead of spec configurations, but ruled it out to give mid-level library users precise control.
* **Multi-Device Coordination**: We ruled out multi-device synchronization (e.g., true wireless stereo) for the MVP to reduce complexity, but kept the `BapUnicastManager` structure to support it in the future.
* **Automatic Negotiation**: We opted for a prescriptive approach rather than automatic fallback negotiation when a peer rejects a configuration, keeping the implementation predictable.
## Testing
* **Unit Tests**: Will be included in each file (e.g., testing mapping logic in `src/mapping.rs` with fake data).
* **Integration Tests**: A new file `tests/tests.rs` will be created to test the public API of the `BapUnicastManager` with mock peer connections.