Basic Audio Profile Unicast library

An implementation of the Basic Audio Profile 1.0.2.

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.
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

#[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.
    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:

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:

    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:

    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:

    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.