Audio Stream Control Service (ASCS)

Audio Stream Control Service (ASCS)
Complete BLE LE Audio Tutorial โ€” How Earbuds, Hearing Aids and Wireless Mics Set Up Audio Streams Over BLE
9
Control Operations
7
ASE States
3
GATT Characteristics
BT 5.2+
Core Requirement

๐Ÿ” Keywords for This Post
ASCS Audio Stream Control Service Audio Stream Endpoint ASE State Machine Sink ASE Source ASE ASE Control Point Config Codec Config QoS CIS CIG LE Audio BAP PACS Unicast Audio BlueZ GATT BLE Streaming

What is ASCS and Why Does It Exist?

When you connect a BLE wireless earbud to your phone, several things happen before you hear any audio. The phone first needs to find out what audio capabilities the earbud supports. Then it must agree on a codec, set up a wireless “pipe” with specific quality settings, and finally tell the earbud to start listening. All of this negotiation goes through ASCS โ€” the Audio Stream Control Service.

ASCS is a GATT-based Bluetooth service introduced as part of the LE Audio stack in Bluetooth Core Spec 5.2. It runs on devices like earbuds, hearing aids, headsets, and wireless microphones โ€” basically anything that needs to send or receive audio over a BLE unicast connection.

The service exposes a set of characteristics that let a client (your phone or laptop) discover, configure, and control Audio Stream Endpoints (ASEs) on the server (the audio device). An ASE is simply an audio endpoint โ€” a Sink ASE receives audio (speakers), a Source ASE transmits audio (microphone).

Important distinction: ASCS only sets up and controls the audio stream. The actual audio data travels over a CIS (Connected Isochronous Stream), not over GATT. Think of ASCS as the “control plane” and CIS as the “data plane.”

๐Ÿ“ˆ Where ASCS Fits in the BLE Audio Stack

Before diving deep, here is a simple view of where ASCS sits in the overall LE Audio stack. Understanding this context will help you avoid confusion about what ASCS does vs what other layers handle.

Application Layer (Music Player, Call App, Gaming)
โ†“
BAP โ€” Basic Audio Profile (orchestrates everything)
โ†“
ASCS (stream control) ย +ย  PACS (codec capabilities)
โ†“
GATT / ATT (reads and writes characteristics)
โ†“
L2CAP / LE-ACL (BLE connection for control messages)
โ†“
CIS / CIG โ€” Audio Data (isochronous channel for actual audio)

Key Insight: PACS tells the client what audio codecs the device supports. ASCS is what the client uses to say “OK, I want to use LC3 at 16 kHz โ€” set it up!” The actual audio then flows over a CIS, not over GATT.

๐Ÿ“– Key Concepts You Must Know First

These terms come up throughout the entire spec. Get these clear in your head before reading further.

Term What It Means in Plain Language
ASE (Audio Stream Endpoint) A logical audio channel on the server side. Like a socket โ€” audio data flows in or out of it. The server (earbud) owns the ASE; the client (phone) configures it.
Sink ASE An ASE where audio data flows into the server. The server is the Audio Sink โ€” like a speaker receiving music. Audio comes from the phone and goes into the earbud.
Source ASE An ASE where audio data flows out of the server. The server is the Audio Source โ€” like a microphone in the earbud sending your voice to the phone.
ASE_ID A unique number the server assigns to each ASE for a given client. Think of it as the ASE’s name or handle โ€” you refer to it in all control operations.
CIS (Connected Isochronous Stream) A BLE isochronous channel dedicated to carrying audio data. Once an ASE is in Streaming state, it is “coupled” to a CIS. The actual audio SDUs travel here, not over GATT.
CIG (Connected Isochronous Group) A group of CIS streams that are synchronized together. Useful for stereo earbuds where both left and right channels must be in sync. Identified by CIG_ID.
QoS (Quality of Service) Configuration parameters that define the reliability and latency characteristics of the audio stream. Includes SDU Interval, PHY type, Max SDU size, Retransmission Number, Max Transport Latency, and Presentation Delay.
Presentation Delay How much time (in microseconds) the audio device needs from when it receives audio data to when it actually plays it. This allows for buffering and synchronization between earbuds.
ASE Control Point A special GATT characteristic that acts as the command channel. The client writes operations (like Config Codec, Enable, Release) to this characteristic. The server responds with notifications.
Client vs Server in ASCS In ASCS, the server is the audio device (earbud, hearing aid). The client is the controlling device (phone, laptop). The server hosts the GATT service; the client reads and writes it.

๐Ÿ”Œ ASCS GATT Characteristics

ASCS exposes exactly three types of GATT characteristics. There can be multiple instances of Sink ASE and Source ASE characteristics, but only one instance of the ASE Control Point.

Characteristic UUID Properties Security Count on Server
Sink ASE ยซSink ASEยป Read, Notify Encryption Required One or more (one per concurrent sink stream)
Source ASE ยซSource ASEยป Read, Notify Encryption Required One or more (one per concurrent source stream)
ASE Control Point ยซASE Control Pointยป Write, WriteWithoutResponse, Notify Encryption Required Exactly one (controls all ASEs)

Important Rules: At least one of Sink ASE or Source ASE must be supported (you cannot have an ASCS with neither). There can be only one ASCS instance on a server. The service UUID must be set to ยซAudio Stream Controlยป as defined in Bluetooth Assigned Numbers.

Multi-Client Isolation

One clever design in ASCS is that the server exposes separate ASE characteristic values for each connected client. Even though there is only one GATT attribute handle for the Sink ASE characteristic, Client A and Client B each get their own independent copy of the value. Client A’s configuration does not affect Client B’s view of the ASE, and vice versa.

Example: An earbud that supports 3 concurrent unicast streams for a single client would expose 3 ASE characteristics โ€” maybe two Sink ASEs (music + voice) and one Source ASE (microphone). Each characteristic can serve one stream per client.

โš™ ASE State Machine โ€” The Heart of ASCS

Every ASE has its own state machine running on the server. When you understand the state machine, you understand ASCS. All the control operations are just ways to move between these states.

The 7 States

State Name Value What It Means ASE Type
Idle 0x00 No codec config, no QoS config. ASE is doing nothing. All
Codec Configured 0x01 A codec has been agreed upon. Server also exposes its preferred QoS params here. No QoS config yet. All
QoS Configured 0x02 Codec and QoS both configured. CIG/CIS IDs assigned. A CIS may exist but ASE is not coupled to it yet. All
Enabling 0x03 Codec + QoS + Metadata applied. CIS may exist but ASE is not yet coupled. Getting ready to stream. Risk of lost packets if streaming starts now. All
Streaming 0x04 ASE is coupled to a CIS. Audio Sink has signaled Receiver Start Ready. Audio data is actively flowing. All
Disabling 0x05 ASE is being decoupled from CIS. Source ASE keeps transmitting until Audio Sink sends Receiver Stop Ready. Metadata still associated. Source ASE only
Releasing 0x06 CIS is being torn down or already disconnected. QoS config is gone. Codec config may or may not be cached. Metadata gone. All

Sink ASE Flow (Happy Path)

For a Sink ASE (like a speaker in an earbud), the normal audio setup and teardown path looks like this:

IDLE
โ†’ Config Codec (client writes opcode 0x01)
โ†“
CODEC CONFIGURED
โ†’ Config QoS (client writes opcode 0x02)
โ†“
QoS CONFIGURED
โ†’ Enable (client writes opcode 0x03)
โ†“
ENABLING
โ†’ Receiver Start Ready (server/client writes opcode 0x04)
โ†“
STREAMING โ–ถ Audio Flowing
โ†’ Disable (opcode 0x05) โ†’ goes directly to QoS Configured (Sink ASE only)
โ†“ Release (opcode 0x08)
RELEASING
โ†’ Released โ€” server autonomously goes to IDLE (no cache) or CODEC CONFIGURED (cache)

Source ASE Difference โ€” The Disabling State

A Source ASE (microphone) behaves slightly differently when disabled. Instead of jumping directly to QoS Configured, it goes through a Disabling intermediate state. This is because the audio source keeps transmitting until the receiver (phone) explicitly signals it is ready to stop receiving. This gives the receiver time to finish processing buffered audio.

STREAMING (Source ASE)
โ†’
DISABLING
โ†’
QoS CONFIGURED
Disable (0x05) Receiver Stop Ready (0x06) Back to configured state

Sink ASE Disable: Sink ASE skips the Disabling state entirely. Disable operation takes a Sink ASE directly from Enabling or Streaming to QoS Configured.

Link Loss Behavior

What happens if the BLE connection drops unexpectedly? ASCS has specific rules:

  • CIS link loss while in Streaming or Disabling: Server immediately transitions that ASE to QoS Configured state.
  • LE-ACL link loss (the BLE connection itself) in any state: Server immediately transitions all affected ASEs to Releasing state, then runs the Released operation.

๐Ÿ”ง ASE Control Operations โ€” Complete Reference

All control operations are written to the ASE Control Point characteristic. Every write starts with a 1-byte opcode, followed by the operation parameters. The server responds with a notification on the same characteristic.

Opcode Operation Valid In State Who Initiates
0x01 Config Codec Idle, Codec Configured, QoS Configured Client or Server
0x02 Config QoS Codec Configured, QoS Configured Client only
0x03 Enable QoS Configured Client only
0x04 Receiver Start Ready Enabling Client or Server (Audio Sink side)
0x05 Disable Enabling, Streaming Client or Server
0x06 Receiver Stop Ready Disabling (Source ASE only) Client only (Audio Sink side)
0x07 Update Metadata Enabling, Streaming Client or Server
0x08 Release Codec Configured through Disabling Client or Server
โ€” Released Releasing only Server only (autonomous)

๐ŸŽถ Operation 1: Config Codec (Opcode 0x01)

Purpose: Agree on which audio codec to use, and at what configuration (sample rate, frame duration, etc.). This is the very first step in setting up any audio stream. Once this succeeds, the server moves to Codec Configured state and also tells the client its preferred QoS parameters (like what PHY it wants).

Both the client and server can initiate Config Codec. A server might autonomously configure itself when it powers on.

Packet Format

Field Size (Bytes) Description
Opcode 1 0x01 = Config Codec
Number_of_ASEs 1 How many ASEs to configure. Must be โ‰ฅ 1. You can configure multiple ASEs in one write.
ASE_ID[i] 1 ID of the ASE to configure (from ASE characteristic)
Target_Latency[i] 1 0x01 = Low Latency, 0x02 = Balanced, 0x03 = High Reliability. Hints to server for QoS preferences.
Target_PHY[i] 1 0x01=LE 1M, 0x02=LE 2M, 0x03=LE Coded. Target PHY to achieve latency goal.
Codec_ID[i] 5 Byte 0: Coding Format (e.g. 0x06 = LC3). Bytes 1โ€“2: Company_ID (0x0000 for standard codecs). Bytes 3โ€“4: Vendor-specific ID (0x0000 for standard).
Codec_Specific_Config_Length[i] 1 Length of the codec config that follows. Can be 0.
Codec_Specific_Config[i] Variable LTV-formatted codec parameters (sample rate, frame duration, octets per frame, etc.). Defined by BAP spec.

Example: Configuring LC3 at 16 kHz on ASE #1

/*
 * Config Codec operation โ€” write to ASE Control Point characteristic
 * Configuring ASE_ID=1 with LC3, 16kHz, balanced latency, LE 2M PHY
 *
 * Byte layout of the write:
 */

uint8_t config_codec_op[] = {
    0x01,       /* Opcode: Config Codec */
    0x01,       /* Number_of_ASEs: 1 */

    /* ASE[0] parameters */
    0x01,       /* ASE_ID: 1 */
    0x02,       /* Target_Latency: 0x02 = Balanced latency and reliability */
    0x02,       /* Target_PHY: 0x02 = LE 2M PHY */

    /* Codec_ID (5 bytes) */
    0x06,       /* Coding_Format: LC3 */
    0x00, 0x00, /* Company_ID: 0x0000 (standard codec) */
    0x00, 0x00, /* Vendor_Codec_ID: 0x0000 (standard codec) */

    /* Codec_Specific_Configuration (LTV format, defined by BAP) */
    0x10,       /* Codec_Specific_Config_Length: 16 bytes */

    /* LTV: Sampling_Frequency = 16000 Hz (0x03) */
    0x02,       /* Length */
    0x01,       /* Type: Sampling_Frequency */
    0x03,       /* Value: 0x03 = 16000 Hz */

    /* LTV: Frame_Duration = 10ms (0x01) */
    0x02,       /* Length */
    0x02,       /* Type: Frame_Duration */
    0x01,       /* Value: 0x01 = 10ms */

    /* LTV: Octets_per_Codec_Frame = 40 */
    0x03,       /* Length */
    0x04,       /* Type: Octets_per_Codec_Frame */
    0x28, 0x00  /* Value: 40 bytes (little-endian) */
};

/*
 * Write this buffer to the ASE Control Point characteristic
 * using GATT Write Characteristic Value or Write Without Response
 */
bt_gatt_write(conn, ase_ctrl_point_handle, config_codec_op, sizeof(config_codec_op));

/*
 * Server will respond with a notification on ASE Control Point:
 * [Opcode=0x01][Num_ASEs=1][ASE_ID=1][Response_Code=0x00][Reason=0x00]
 *
 * Then server sends notification on Sink ASE characteristic with:
 * [ASE_ID=1][ASE_State=0x01 (Codec Configured)][Framing][Preferred_PHY]...
 */

After Success: The server responds on the ASE characteristic with state = 0x01 (Codec Configured) and also fills in its preferred QoS parameters โ€” Preferred_PHY, Preferred_Retransmission_Number, Max_Transport_Latency, Presentation_Delay_Min, Presentation_Delay_Max. These are hints for the next step (Config QoS).

โšก Operation 2: Config QoS (Opcode 0x02)

Purpose: Set the Quality of Service parameters for the CIS that will carry this ASE’s audio. You are telling the server: “I will create a CIS with CIG_ID X and CIS_ID Y, with these exact timing and reliability parameters.” Only the client can initiate this operation.

The parameters you send here map directly to the HCI LE Set CIG Parameters command (HCI_LE_Set_CIG_Parameters). So the client’s host layer uses these values when it sets up the CIG/CIS at the controller level.

Packet Format

Field Size (Bytes) Description
Opcode 1 0x02 = Config QoS
Number_of_ASEs 1 Number of ASEs. โ‰ฅ 1.
ASE_ID[i] 1 ASE identifier
CIG_ID[i] 1 Connected Isochronous Group ID. Groups related CIS streams (e.g. left+right earbud).
CIS_ID[i] 1 Connected Isochronous Stream ID within the CIG. Must be unique per direction per client.
SDU_Interval[i] 3 Time interval between audio frames in microseconds. Range: 0xFF to 0xFFFFF. E.g. 10000 = 10ms frame interval.
Framing[i] 1 0x00 = Unframed ISOAL PDUs, 0x01 = Framed. Unframed is simpler and preferred when SDU size is fixed.
PHY[i] 1 Bit 0=LE 1M, Bit 1=LE 2M, Bit 2=LE Coded. Use the server’s Preferred_PHY as a guide.
Max_SDU[i] 2 Maximum size of a single audio SDU (frame) in bytes. Range: 0โ€“0xFFF.
Retransmission_Number[i] 1 How many times to retransmit each audio packet. Higher = more reliable but more latency/power.
Max_Transport_Latency[i] 2 Maximum end-to-end transport latency in milliseconds. Range: 5โ€“4000ms.
Presentation_Delay[i] 3 How long the server needs to buffer and render audio after receiving it, in microseconds. Must be within the server’s stated Min/Max range.

Example: Config QoS for LC3 16kHz

/*
 * Config QoS operation for ASE_ID=1
 * Setting up CIG_ID=0x01, CIS_ID=0x01 with 10ms frame interval
 */

uint8_t config_qos_op[] = {
    0x02,           /* Opcode: Config QoS */
    0x01,           /* Number_of_ASEs: 1 */

    0x01,           /* ASE_ID: 1 */
    0x01,           /* CIG_ID: 1 */
    0x01,           /* CIS_ID: 1 */

    /* SDU_Interval: 10000 us = 10ms (3 bytes, little-endian) */
    0x10, 0x27, 0x00,

    0x00,           /* Framing: 0x00 = Unframed */
    0x02,           /* PHY: LE 2M */

    /* Max_SDU: 40 bytes (2 bytes little-endian) */
    0x28, 0x00,

    0x02,           /* Retransmission_Number: 2 */

    /* Max_Transport_Latency: 10ms = 10 (2 bytes little-endian) */
    0x0A, 0x00,

    /* Presentation_Delay: 40000 us (3 bytes little-endian) */
    0x40, 0x9C, 0x00
};

bt_gatt_write(conn, ase_ctrl_point_handle, config_qos_op, sizeof(config_qos_op));

/*
 * On success, server notifies ASE characteristic with:
 * ASE_State = 0x02 (QoS Configured)
 * Additional params include: CIG_ID, CIS_ID, SDU_Interval, Framing,
 * PHY, Max_SDU, Retransmission_Number, Max_Transport_Latency, Presentation_Delay
 */

Rule to remember: Two Sink ASEs for the same client cannot share the same CIG_ID + CIS_ID combination. Similarly for Source ASEs. If you try this, the server rejects with Response_Code 0x09, Reason 0x0A (Invalid_ASE_CIS_Mapping).

โ–ถ Operation 3: Enable (Opcode 0x03)

Purpose: Tell the server to enable the ASE and apply any Metadata. This moves the ASE to Enabling state. The CIS can now be established (though the ASE is not yet coupled to it). Only the client initiates Enable.

Metadata in this context means audio context type information โ€” things like “this audio is voice” or “this audio is media” or “this audio is for notifications.” This helps devices make decisions about routing, processing, and priority.

Packet Format

Field Size (Bytes) Description
Opcode 1 0x03 = Enable
Number_of_ASEs 1 Number of ASEs to enable
ASE_ID[i] 1 ASE identifier
Metadata_Length[i] 1 Length of Metadata. Can be 0 if no metadata.
Metadata[i] Variable LTV-formatted metadata. Only present if Metadata_Length > 0. Streaming_Audio_Contexts is the most common type.
/*
 * Enable operation โ€” ASE_ID=1, with Media audio context metadata
 *
 * Streaming_Audio_Contexts LTV:
 *   Type 0x02 = Streaming_Audio_Contexts
 *   Value: bitmask โ€” 0x0004 = Media
 */

uint8_t enable_op[] = {
    0x03,           /* Opcode: Enable */
    0x01,           /* Number_of_ASEs: 1 */

    0x01,           /* ASE_ID: 1 */
    0x04,           /* Metadata_Length: 4 bytes of metadata follow */

    /* LTV: Streaming_Audio_Contexts = Media */
    0x03,           /* Length: 3 bytes of LTV content */
    0x02,           /* Type: Streaming_Audio_Contexts */
    0x04, 0x00      /* Value: 0x0004 = Media (little-endian) */
};

bt_gatt_write(conn, ase_ctrl_point_handle, enable_op, sizeof(enable_op));

/*
 * Server moves ASE to Enabling state (0x03)
 *
 * Special case for Sink ASE:
 * If a CIS is already established AND server is acting as Audio Sink
 * AND server is ready to receive audio, the server MAY autonomously
 * call Receiver Start Ready and skip directly to Streaming state
 * without sending a notification for the Enabling state first.
 */

โœ… Operation 4: Receiver Start Ready (Opcode 0x04)

Purpose: This operation is the “green light” signal that says the Audio Sink is ready to start receiving audio. It completes the coupling of the ASE to the CIS and moves the ASE to Streaming state.

Who sends it depends on which side is the Audio Sink:

  • If the server is the Sink (earbud receiving music): The server sends Receiver Start Ready autonomously.
  • If the client is the Sink (phone receiving voice from earbud): The client writes Receiver Start Ready to the ASE Control Point.

Direction Rule: If the client tries to send Receiver Start Ready for a Sink ASE (where the server is already the sink), the server rejects it with Response_Code 0x05 (Invalid ASE direction). This operation is only valid for Source ASEs when initiated by the client.

Packet Format

Field Size (Bytes) Description
Opcode 1 0x04 = Receiver Start Ready
Number_of_ASEs 1 Number of ASEs
ASE_ID[i] 1 ASE identifier
/*
 * Receiver Start Ready โ€” client signals it is ready to receive
 * audio from the Source ASE (Source ASE = server sending voice audio)
 * This is the client acting as Audio Sink for a Source ASE.
 */

uint8_t recv_start_ready[] = {
    0x04,   /* Opcode: Receiver Start Ready */
    0x01,   /* Number_of_ASEs: 1 */
    0x02    /* ASE_ID: 2 (the Source ASE, e.g. microphone) */
};

bt_gatt_write(conn, ase_ctrl_point_handle, recv_start_ready, sizeof(recv_start_ready));

/*
 * Server transitions Source ASE to Streaming state (0x04)
 * Audio data now flows over the CIS
 */

โ–ฎ Operation 5: Disable (Opcode 0x05)

Purpose: Stop the audio stream for an ASE. Behavior differs between Sink and Source ASEs.

  • Sink ASE: Disable moves directly to QoS Configured. No Disabling intermediate state.
  • Source ASE: Disable moves to Disabling state first. The source keeps transmitting until Receiver Stop Ready is sent.

Packet Format

Field Size (Bytes) Description
Opcode 1 0x05 = Disable
Number_of_ASEs 1 Number of ASEs to disable
ASE_ID[i] 1 ASE identifier
/* Disable Sink ASE #1 โ€” goes directly to QoS Configured */
uint8_t disable_sink[] = { 0x05, 0x01, 0x01 };
bt_gatt_write(conn, ase_ctrl_point_handle, disable_sink, sizeof(disable_sink));
/* ASE #1 moves: Streaming โ†’ QoS Configured */

/* Disable Source ASE #2 โ€” goes to Disabling first */
uint8_t disable_source[] = { 0x05, 0x01, 0x02 };
bt_gatt_write(conn, ase_ctrl_point_handle, disable_source, sizeof(disable_source));
/* ASE #2 moves: Streaming โ†’ Disabling */
/* Source ASE stays in Disabling and keeps transmitting */
/* Client must follow with Receiver Stop Ready (0x06) to complete teardown */

โ–  Operation 6: Receiver Stop Ready (Opcode 0x06)

Purpose: The second part of stopping a Source ASE. After Disable puts a Source ASE into Disabling state, the Audio Sink sends Receiver Stop Ready to confirm it has finished consuming audio. This moves the Source ASE back to QoS Configured state.

Only valid for Source ASEs. Only the client (acting as Audio Sink) sends this. The server rejects it with error 0x05 (Invalid ASE direction) if called for a Sink ASE.

/* Receiver Stop Ready for Source ASE #2 */
uint8_t recv_stop_ready[] = { 0x06, 0x01, 0x02 };
bt_gatt_write(conn, ase_ctrl_point_handle, recv_stop_ready, sizeof(recv_stop_ready));
/* Source ASE #2 moves: Disabling โ†’ QoS Configured */

๐Ÿ“„ Operation 7: Update Metadata (Opcode 0x07)

Purpose: Change the Metadata for an active ASE without tearing down and re-establishing the stream. Valid in Enabling and Streaming states. Both client and server can initiate this.

Use case: A call comes in while music is playing. The phone changes the Streaming_Audio_Contexts metadata from “Media” to “Conversational” โ€” without stopping the audio stream and restarting it from scratch.

/*
 * Update Metadata โ€” change audio context from Media to Conversational
 * while ASE is in Streaming state
 */

uint8_t update_metadata_op[] = {
    0x07,           /* Opcode: Update Metadata */
    0x01,           /* Number_of_ASEs: 1 */
    0x01,           /* ASE_ID: 1 */
    0x04,           /* Metadata_Length: 4 bytes */

    /* LTV: Streaming_Audio_Contexts = Conversational (0x0002) */
    0x03,           /* Length */
    0x02,           /* Type: Streaming_Audio_Contexts */
    0x02, 0x00      /* Value: 0x0002 = Conversational */
};

bt_gatt_write(conn, ase_ctrl_point_handle, update_metadata_op, sizeof(update_metadata_op));
/* ASE stays in Streaming state, only metadata changes */

โŒ Operation 8: Release (Opcode 0x08)

Purpose: Completely tear down all resources associated with an ASE. Immediately decouples the ASE from its CIS, tears down the CIS, and frees all configuration. This is the “nuclear option” โ€” it takes you all the way back to Idle or Codec Configured (if server caches the codec config).

Valid from any state that has a configuration โ€” Codec Configured, QoS Configured, Enabling, Streaming, Disabling. Can be initiated by either client or server.

/* Release ASE #1 โ€” from any configured state */
uint8_t release_op[] = { 0x08, 0x01, 0x01 };
bt_gatt_write(conn, ase_ctrl_point_handle, release_op, sizeof(release_op));

/*
 * Server transitions ASE to Releasing state (0x06)
 * Server tears down the CIS
 * Server then autonomously runs Released operation:
 *   - If caching codec: ASE goes to Codec Configured (0x01)
 *   - If not caching: ASE goes to Idle (0x00)
 * Server sends notification on ASE characteristic with final state
 * Server does NOT send notification on ASE Control Point for Released
 */

โ™บ Operation 9: Released (Server Autonomous)

Purpose: This is the final cleanup step after Release or after an LE-ACL link loss. The server runs this autonomously โ€” the client does not write anything. The server decides whether to cache the codec configuration or start fresh from Idle.

Scenario Next State What Server Writes to Characteristic
Server wants to cache codec config Codec Configured (0x01) Saves Codec_ID, Codec_Specific_Config, preferred QoS. Client can skip Config Codec next time.
Server does not cache config Idle (0x00) Deletes all Additional_ASE_Parameters. ASE is completely clean.

No Control Point Notification: Unlike all other operations, the Released operation does NOT generate a notification on the ASE Control Point characteristic. The server only notifies the ASE characteristic itself with the new state.

๐Ÿ“น Complete Audio Stream Setup Flow

Here is the full sequence showing how a phone (Client/Audio Source) sets up a music stream to earbuds (Server/Audio Sink). This is the most common LE Audio unicast scenario.

Phone (Client) GATT / BLE Earbud (Server)
1. Discover ASCS service
Find ASE characteristics and ASE Control Point handle
โ†’ GATT Discover Services โ†’

โ† Service/Characteristic Handles โ†

Returns ASCS UUID, Sink ASE handle, ASE Control Point handle
2. Check supported codecs
Read PACS to know what codecs server supports
โ†’ GATT Read PACS โ†’

โ† PAC Records (LC3 supported) โ†

Returns PAC records showing LC3, supported sample rates, frame durations
3. Subscribe to notifications
Enable CCC descriptor on Sink ASE and ASE Control Point
โ†’ Write CCC = 0x0001 โ†’

โ† OK โ†

Stores CCCD subscription for this client
4. Config Codec (0x01)
Request LC3, 16kHz, 10ms frames on Sink ASE #1
โ†’ Write ASE Control Point โ†’

โ† Notify: CP response (Success) โ†
โ† Notify: ASE State=Codec Configured โ†

ASE moves to Codec Configured (0x01). Server exposes preferred QoS: PHY, retransmission count, presentation delay range
5. Config QoS (0x02)
CIG_ID=1, CIS_ID=1, SDU_Interval=10ms, PHY=LE2M, Max_SDU=40, Presentation_Delay=40ms
โ†’ Write ASE Control Point โ†’

โ† Notify: CP response (Success) โ†
โ† Notify: ASE State=QoS Configured โ†

ASE moves to QoS Configured (0x02). CIG/CIS IDs stored. QoS params stored.
6. Create CIG at HCI level
Client host calls HCI_LE_Set_CIG_Parameters to create CIG_ID=1 in controller
Internal HCI โ†“
(not on BLE link)
No action from server at this point
7. Enable (0x03)
Enable Sink ASE #1 with Media audio context metadata
โ†’ Write ASE Control Point โ†’

โ† Notify: CP response (Success) โ†
โ† Notify: ASE State=Enabling โ†

ASE moves to Enabling (0x03). Server is now aware audio is about to start.
8. Create CIS connection
Client calls HCI_LE_Create_CIS. BLE controller establishes the isochronous channel.
โ†” BLE CIS Setup โ†”
(HCI level)
CIS is now established. Both sides have isochronous channel ready.
Server is Sink โ€” it sends Receiver Start Ready autonomously โ† Notify: ASE State=Streaming โ† 9. Server autonomously sends Receiver Start Ready
ASE coupled to CIS. Earbud is ready to receive audio.
๐ŸŽต Sending Audio SDUs over CIS โ†’ LC3 Audio Frames (CIS) โ†’
ASE State = Streaming (0x04)
๐ŸŽต Receiving and Playing Audio
10. User pauses โ€” Disable (0x05)
Disable Sink ASE #1
โ†’ Write ASE Control Point โ†’

โ† Notify: ASE State=QoS Configured โ†

Sink ASE goes directly to QoS Configured (0x02). CIS may stay up.
11. App closes โ€” Release (0x08)
Release all resources for ASE #1
โ†’ Write ASE Control Point โ†’

โ† Notify: ASE State=Releasing โ†
โ† Notify: ASE State=Idle (or Codec Configured) โ†

CIS torn down. ASE goes to Idle or Codec Configured depending on server caching preference.

๐Ÿ“‹ ASE Characteristic Value Format

When you read a Sink ASE or Source ASE characteristic, this is the format you get back. The server also sends this same format in notifications whenever the ASE state changes.

Base Format (All States)

Byte Offset Field Size Notes
0 ASE_ID 1 byte Never 0x00. Unique per client namespace. Stable if server has bonded relationship with client.
1 ASE_State 1 byte 0x00=Idle, 0x01=Codec Configured, 0x02=QoS Configured, 0x03=Enabling, 0x04=Streaming, 0x05=Disabling, 0x06=Releasing
2+ Additional_ASE_Parameters Variable Empty in Idle and Releasing. Contents depend on state (see below).

Additional_ASE_Parameters in Codec Configured State (0x01)

When ASE_State = 0x01, the Additional_ASE_Parameters include the server’s preferred QoS hints plus the codec configuration:

Framing
1B
Preferred_PHY
1B
Pref_RTx_Num
1B
Max_Transport_Latency
2B
PD_Min
3B
PD_Max
3B
Pref_PD_Min
3B
Pref_PD_Max
3B
Codec_ID
5B
CSC_Len
1B
Codec_Specific_Config
Variable

PD = Presentation Delay, CSC = Codec Specific Configuration, RTx = Retransmission

Reading ASE Characteristic in Code

/*
 * Callback when ASE characteristic notification is received
 * Parse the ASE value to extract state and parameters
 */
void on_ase_notification(const uint8_t *data, uint16_t len)
{
    if (len < 2) {
        /* Minimum is ASE_ID + ASE_State */
        return;
    }

    uint8_t ase_id    = data[0];
    uint8_t ase_state = data[1];

    printf("ASE_ID: %d, State: %d\n", ase_id, ase_state);

    switch (ase_state) {
    case 0x00: /* Idle */
        printf("ASE %d is Idle โ€” no configuration\n", ase_id);
        break;

    case 0x01: /* Codec Configured */
        if (len >= 2 + 1 + 1 + 1 + 2 + 3 + 3 + 3 + 3 + 5 + 1) {
            uint8_t framing        = data[2];
            uint8_t pref_phy       = data[3];
            uint8_t pref_rtx       = data[4];
            uint16_t max_latency   = data[5] | (data[6] << 8);
            /* Presentation_Delay_Min at data[7..9] (3 bytes LE) */
            uint32_t pd_min = data[7] | (data[8] << 8) | (data[9] << 16);
            /* Presentation_Delay_Max at data[10..12] */
            uint32_t pd_max = data[10] | (data[11] << 8) | (data[12] << 16);

            printf("  Framing: %s\n", framing ? "Framed" : "Unframed");
            printf("  Preferred PHY: 0x%02X\n", pref_phy);
            printf("  Preferred RTx Number: %d\n", pref_rtx);
            printf("  Max Transport Latency: %d ms\n", max_latency);
            printf("  Presentation Delay: %d us - %d us\n", pd_min, pd_max);
        }
        break;

    case 0x02: /* QoS Configured */
        printf("ASE %d โ€” QoS configured, CIG_ID=%d CIS_ID=%d\n",
               ase_id, data[2], data[3]);
        break;

    case 0x03: /* Enabling */
        printf("ASE %d โ€” Enabling, CIG_ID=%d CIS_ID=%d\n",
               ase_id, data[2], data[3]);
        break;

    case 0x04: /* Streaming */
        printf("ASE %d โ€” STREAMING! Audio data flowing.\n", ase_id);
        break;

    case 0x05: /* Disabling */
        printf("ASE %d โ€” Disabling (Source ASE decoupling)\n", ase_id);
        break;

    case 0x06: /* Releasing */
        printf("ASE %d โ€” Releasing resources\n", ase_id);
        break;

    default:
        printf("ASE %d โ€” Unknown state 0x%02X\n", ase_id, ase_state);
    }
}

โš  Error Response Codes

When the server rejects or cannot complete an operation, it notifies the ASE Control Point with a Response_Code and a Reason. Understanding these helps you debug ASCS issues.

Response_Code Name What Went Wrong
0x00 Success Operation completed successfully.
0x01 Unsupported Opcode You wrote an opcode the server does not recognize or support.
0x02 Invalid Length Number_of_ASEs field does not match actual parameter count, or total length is wrong.
0x03 Invalid ASE_ID You referenced an ASE_ID that does not exist for this client.
0x04 Invalid ASE State Machine Transition You tried to do an operation that is not valid for the current state. E.g. Enable on an Idle ASE (must be QoS Configured first).
0x05 Invalid ASE Direction Receiver Start Ready or Receiver Stop Ready called on a Sink ASE instead of Source ASE.
0x06 Unsupported Audio Capabilities You requested a codec config that was not in any of the server’s PAC records.
0x07 Unsupported Config Parameter Value Server does not support one of the values you gave. Reason byte tells you which parameter: 0x01=Codec_ID, 0x03=SDU_Interval, 0x05=PHY, 0x09=Presentation_Delay, etc.
0x08 Rejected Config Parameter Value Server supports this parameter range in general, but rejects the specific value in the current context (e.g. resource conflict).
0x09 Invalid Config Parameter Value The value you wrote is outside the allowed range entirely. Common with Presentation_Delay outside Min/Max range.
0x0D Insufficient Resources Server cannot complete the operation โ€” it is out of ASEs, memory, or processing capacity.
0x0E Unspecified Error Something went wrong on the server that does not fit any other category.

Reading the Control Point Notification Response

/*
 * ASE Control Point notification format when responding to a client operation:
 *
 * [Opcode: 1B] [Number_of_ASEs: 1B] [ASE_ID: 1B] [Response_Code: 1B] [Reason: 1B]
 * ... repeated for each ASE in the original request ...
 *
 * Special case for 0x01 (Unsupported Opcode) and 0x02 (Invalid Length):
 * Number_of_ASEs = 0xFF, ASE_ID = 0x00, Reason = 0x00
 */

void on_cp_notification(const uint8_t *data, uint16_t len)
{
    if (len < 2) return;

    uint8_t opcode       = data[0];
    uint8_t num_ases     = data[1];

    /* Check for whole-operation errors first */
    if (num_ases == 0xFF) {
        uint8_t resp_code = data[3]; /* 0x01 or 0x02 */
        printf("Operation 0x%02X rejected: %s\n", opcode,
               resp_code == 0x01 ? "Unsupported Opcode" : "Invalid Length");
        return;
    }

    /* Per-ASE responses */
    uint16_t offset = 2;
    for (int i = 0; i < num_ases; i++) {
        if (offset + 3 > len) break;
        uint8_t ase_id    = data[offset];
        uint8_t resp_code = data[offset + 1];
        uint8_t reason    = data[offset + 2];

        if (resp_code == 0x00) {
            printf("ASE %d: Operation 0x%02X SUCCESS\n", ase_id, opcode);
        } else {
            printf("ASE %d: FAILED โ€” Response_Code=0x%02X Reason=0x%02X\n",
                   ase_id, resp_code, reason);
        }
        offset += 3;
    }
}

๐Ÿ‘ฅ ASCS Multi-Client Isolation Rules

This section is important if you are implementing ASCS on a server that can handle multiple connected clients at once.

  • Each client gets its own ASE namespace: ASE_ID values are per-client. Client A’s ASE_ID=1 and Client B’s ASE_ID=1 are completely separate ASEs even if they share the same GATT attribute handle.
  • Separate characteristic values per client: When a client reads the Sink ASE characteristic, it sees only the value associated with itself. It cannot see another client’s state.
  • ASE_ID stability: If the server has a bonded (trusted) relationship with a client, the ASE_ID for that client must not change across reconnections. This allows the client to remember which ASE is which.
  • ASE_ID must not be 0x00: The value 0x00 is reserved and the server must never assign it as an ASE_ID.
  • Server controls resource allocation: If Client A is using all the server’s ASEs in Streaming state, the server can refuse to enable ASEs for Client B. Or it can decide to release Client A’s ASEs to accommodate Client B. This is implementation-defined.

๐Ÿ”— GATT Sub-Procedure Requirements

These GATT sub-procedures are mandatory on any ASCS server:

GATT Sub-Procedure Requirement Used For
Write Characteristic Value Mandatory Writing control operations to ASE Control Point
Write Without Response Mandatory Faster writes to ASE Control Point when ACK not needed
Notifications Mandatory Server notifies ASE state changes and CP responses
Read Characteristic Descriptors Mandatory Client reads CCCD to check if notifications are enabled
Write Characteristic Descriptors Mandatory Client writes CCCD to enable/disable notifications
Write Long Characteristic Value Mandatory For ASE Control Point writes that exceed ATT_MTU

Encryption is Required: All three ASCS characteristics require an encrypted BLE connection. You must pair/bond before you can interact with ASCS. This is why ASCS works only on encrypted links.

๐Ÿฆ BlueZ โ€” ASCS in Practice

If you are working with BlueZ on Linux, ASCS is handled by the bluetooth/audio/ subsystem. Here are the key points to know.

BlueZ DBus Interface for ASCS

In BlueZ, ASCS is exposed through the org.bluez.MediaEndpoint1 DBus interface. When you use A2DP LE Audio, BlueZ internally handles ASCS negotiations. However, for direct GATT-level access you interact with the GATT characteristics directly.

#!/bin/bash
# Using bluetoothctl to explore ASCS on a connected LE Audio device

# Connect to the device (must be BLE LE Audio capable)
bluetoothctl connect AA:BB:CC:DD:EE:FF

# List all GATT attributes - look for ASCS UUID: 0x184E
bluetoothctl -- list-attributes AA:BB:CC:DD:EE:FF

# Read the Sink ASE characteristic
# (replace the UUID/path with the actual one from list-attributes)
bluetoothctl -- select-attribute /org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service001a/char001b
bluetoothctl -- read

# Example output: [0x01, 0x00]
# ASE_ID=1, ASE_State=0x00 (Idle)

# Subscribe to notifications on Sink ASE
bluetoothctl -- notify on

Python Script to Interact with ASCS (using bluepy/dbus)

#!/usr/bin/env python3
"""
ASCS exploration script using BlueZ DBus interface
Reads ASE characteristics and subscribes to state change notifications
"""

import dbus
import dbus.mainloop.glib
from gi.repository import GLib

# ASCS Service UUID
ASCS_UUID         = "0000184e-0000-1000-8000-00805f9b34fb"
SINK_ASE_UUID     = "00002bc4-0000-1000-8000-00805f9b34fb"
SOURCE_ASE_UUID   = "00002bc5-0000-1000-8000-00805f9b34fb"
ASE_CP_UUID       = "00002bc6-0000-1000-8000-00805f9b34fb"

# State names for human-readable output
ASE_STATES = {
    0x00: "Idle",
    0x01: "Codec Configured",
    0x02: "QoS Configured",
    0x03: "Enabling",
    0x04: "Streaming",
    0x05: "Disabling",
    0x06: "Releasing"
}

def parse_ase_value(data):
    """Parse ASE characteristic value and print human-readable info"""
    if len(data) < 2:
        print("  [ERROR] ASE value too short")
        return

    ase_id    = data[0]
    ase_state = data[1]
    state_name = ASE_STATES.get(ase_state, f"Unknown (0x{ase_state:02X})")

    print(f"  ASE_ID: {ase_id}")
    print(f"  ASE_State: {ase_state:#04x} = {state_name}")

    if ase_state == 0x01:  # Codec Configured
        if len(data) >= 4:
            framing   = data[2]
            pref_phy  = data[3]
            print(f"  Framing: {'Unframed supported' if framing == 0 else 'Framed only'}")
            print(f"  Preferred PHY: {pref_phy:#04x}")

    elif ase_state == 0x02:  # QoS Configured
        if len(data) >= 4:
            cig_id = data[2]
            cis_id = data[3]
            print(f"  CIG_ID: {cig_id}, CIS_ID: {cis_id}")

    elif ase_state in (0x03, 0x04, 0x05):  # Enabling/Streaming/Disabling
        if len(data) >= 4:
            cig_id = data[2]
            cis_id = data[3]
            print(f"  CIG_ID: {cig_id}, CIS_ID: {cis_id}")
            if len(data) > 4:
                meta_len = data[4]
                print(f"  Metadata Length: {meta_len}")


def build_config_codec_op(ase_id):
    """
    Build Config Codec operation bytes for LC3 at 16kHz, 10ms frames
    Opcode=0x01, single ASE, LC3 codec, balanced latency, LE 2M PHY
    """
    return bytes([
        0x01,       # Opcode: Config Codec
        0x01,       # Number_of_ASEs: 1
        ase_id,     # ASE_ID
        0x02,       # Target_Latency: Balanced
        0x02,       # Target_PHY: LE 2M
        # Codec_ID (5 bytes): LC3 standard
        0x06, 0x00, 0x00, 0x00, 0x00,
        # Codec_Specific_Config (LTV format)
        0x10,       # Config length: 16 bytes
        # LTV: Sampling_Frequency = 16000 Hz
        0x02, 0x01, 0x03,
        # LTV: Frame_Duration = 10ms
        0x02, 0x02, 0x01,
        # LTV: Octets_per_Codec_Frame = 40
        0x03, 0x04, 0x28, 0x00,
        # LTV: Codec_Frames_per_SDU = 1
        0x02, 0x05, 0x01,
    ])


# Main interaction flow
if __name__ == "__main__":
    print("ASCS Tutorial โ€” Read ASE state and print info")
    print("In a real application, you would:")
    print("1. Connect to the BLE device (must be bonded/encrypted)")
    print("2. Discover ASCS service (UUID 0x184E)")
    print("3. Find Sink ASE, Source ASE, ASE Control Point characteristics")
    print("4. Subscribe to notifications on all three")
    print("5. Write Config Codec to ASE Control Point to start setup")
    print("6. Follow the state machine flow shown in this tutorial")

Common Implementation Mistakes

Mistake What Happens Fix
Sending Enable before Config QoS Server responds 0x04 (Invalid ASE State Machine Transition) Always follow: Config Codec โ†’ Config QoS โ†’ Enable
Not subscribing to notifications before writing You miss all response notifications. State changes are invisible. Always write CCC=0x0001 on ASE and CP characteristics first
Presentation_Delay outside server’s supported range Server responds 0x09, Reason 0x09 (Invalid_Presentation_Delay) Read PD_Min and PD_Max from ASE characteristic after Config Codec
Two Sink ASEs with same CIG_ID+CIS_ID Server responds 0x09, Reason 0x0A (Invalid_ASE_CIS_Mapping) Each Sink ASE must have a unique CIS_ID within the same CIG
Trying to interact with ASCS without encryption ATT error โ€” Insufficient Encryption Bond/pair before accessing ASCS characteristics
Sending Receiver Stop Ready for a Sink ASE Server responds 0x05 (Invalid ASE Direction) Receiver Stop Ready is only for Source ASEs in Disabling state

Summary โ€” What You Learned

ASCS is the GATT service that provides the control plane for LE Audio unicast streams. Here is what you should now know:

  • โœ… What ASCS does: Lets a client configure and control Audio Stream Endpoints (ASEs) on a server device like earbuds.
  • โœ… Three characteristics: Sink ASE (readable, notifiable), Source ASE (readable, notifiable), ASE Control Point (writable, notifiable).
  • โœ… Seven ASE states: Idle โ†’ Codec Configured โ†’ QoS Configured โ†’ Enabling โ†’ Streaming (with Disabling for Source ASEs and Releasing for teardown).
  • โœ… Nine control operations: Config Codec, Config QoS, Enable, Receiver Start Ready, Disable, Receiver Stop Ready, Update Metadata, Release, Released.
  • โœ… Key sequence: Config Codec โ†’ Config QoS โ†’ Enable โ†’ Receiver Start Ready โ†’ Streaming.
  • โœ… Sink vs Source ASE difference: Sink ASE skips Disabling state. Source ASE needs the client to send Receiver Stop Ready after Disable.
  • โœ… Multi-client isolation: Each client gets its own private ASE namespace and characteristic values. Clients cannot interfere with each other.
  • โœ… Encryption required: All ASCS characteristics require an encrypted BLE link.

Explore More BLE LE Audio Topics

ASCS is one piece of the LE Audio puzzle. Learn about the related specifications that ASCS depends on and works with.

Published Audio Capabilities Service (PACS) Basic Audio Profile (BAP) BLE GATT Deep Dive

Leave a Reply

Your email address will not be published. Required fields are marked *