bluetooth le audio course – Audio Input State Deep Dive

 

 

bluetooth le audio course – Audio Input State Deep Dive
Part 3 of 5  |  All 4 Fields + Gain Setting Properties Explained
4 Fields
in 4 Bytes
4 Gain Modes
Manual / Auto
8-bit rollover
Change_Counter

The Most Important Characteristic in AICS

If AICS were a dashboard, the Audio Input State characteristic is the main display panel. It packs four pieces of information into four consecutive bytes: how loud the input is, whether it is muted, how the gain is being controlled, and a counter that prevents race conditions. Understanding every bit of this 4-byte value is essential before you can write correct BlueZ code.

Byte-Level Layout of Audio Input State

Audio Input State Characteristic — 4 Bytes on the Wire
Byte 0 Byte 1 Byte 2 Byte 3
Gain_Setting
Format: int8
Range: -128 to +127
Mute
Format: uint8
Values: 0, 1, 2
Gain_Mode
Format: uint8
Values: 0, 1, 2, 3
Change_Counter
Format: uint8
Range: 0–255 (wraps)
Example: 0x03
(= +3 gain steps)
Example: 0x00
(= Not Muted)
Example: 0x02
(= Manual)
Example: 0x07
(= counter = 7)
Complete 4-byte read: 0x03 0x00 0x02 0x07 means Gain=+3, NotMuted, Manual, CC=7

Field 1 — Gain_Setting (Byte 0, signed int8)

The Gain_Setting field controls the amplitude of the audio input. It is a signed 8-bit integer (int8), meaning it can hold values from -128 to +127. The value is not directly in decibels — it is a step count. Each step corresponds to a number of 0.1 dB units as specified in the Gain Setting Properties characteristic.

Think of it this way: if Gain_Setting_Units = 5, then each step is 5 × 0.1 dB = 0.5 dB. Moving from Gain_Setting 0 to Gain_Setting 3 increases the input by 1.5 dB. A value of 0 means no change from the input’s baseline amplitude.

Gain Calculation Example — Units=10 (= 1.0 dB per step)
Gain_Setting Value Calculation Actual Gain Change (dB) Effect on Microphone
-5 -5 × 1.0 dB -5.0 dB Quieter input
-1 -1 × 1.0 dB -1.0 dB Slightly quieter
0 0 × 1.0 dB 0 dB (no change) Baseline — original amplitude
+3 +3 × 1.0 dB +3.0 dB Louder input
+10 +10 × 1.0 dB +10.0 dB Significantly louder input
Gain_Setting_Units = 10 means each step = 10 × 0.1 dB = 1.0 dB  |  The valid range is [Gain_Setting_Minimum, Gain_Setting_Maximum]

Gain Setting Properties — The Constraint Characteristic

Before a client can safely set the gain, it must read the Gain Setting Properties characteristic to find out the valid range and step size. This is a 3-byte read-only characteristic that never changes during a connection.

Gain Setting Properties — 3-Byte Layout
Byte 0 Byte 1 Byte 2
Gain_Setting_Units
Format: uint8
Unit = 0.1 dB per step
Gain_Setting_Minimum
Format: int8
Lowest valid Gain_Setting
Gain_Setting_Maximum
Format: int8
Highest valid Gain_Setting
Example: 0x0A
(= 10 → 1.0 dB/step)
Example: 0xED
(= -19 in int8)
Example: 0x0E
(= +14 in int8)
Example: Units=10, Min=-19, Max=+14 means gain adjustable from -19.0 dB to +14.0 dB in 1.0 dB steps

When a client later writes a Set Gain Setting command, the server will reject values below Minimum or above Maximum with error code 0x83 (Value Out of Range). So always read properties first, clamp your gain value, then write.

/* BlueZ client: read Gain Setting Properties before setting gain */ struct gain_properties { uint8_t units; /* 0.1 dB per unit */ int8_t minimum; int8_t maximum; }; /* After reading 3 bytes from Gain Setting Properties handle */ static int8_t clamp_gain(struct gain_properties *props, int8_t requested) { if (requested < props->minimum) return props->minimum; if (requested > props->maximum) return props->maximum; return requested; } /* Usage example */ struct gain_properties props = { .units = 10, .minimum = -19, .maximum = 14 }; int8_t safe_gain = clamp_gain(&props, 20); /* 20 clamped to 14 */ int8_t safe_gain2 = clamp_gain(&props, -25); /* -25 clamped to -19 */

Field 2 — Mute (Byte 1, uint8)

The Mute field has three possible values. Most people expect only two (muted / unmuted) but AICS adds a third state: Disabled. This state exists for privacy-critical devices like hearing aids that have a physical privacy switch the user can press. When the hardware sets Mute to Disabled, remote BLE commands to mute or unmute are simply rejected — only the physical switch can change it back.

Mute Field — State Machine

0
Not Muted
Audio output is live
Opcode
0x02/0x03
1
Muted
Output silenced by BLE command
Hardware
switch only
2
Disabled
BLE mute/unmute rejected (0x82)
Key rule: Gain_Setting value is NEVER changed by a Mute operation. Muting does not set gain to Minimum. They are independent fields.

Field 3 — Gain_Mode (Byte 2, uint8)

The Gain_Mode field tells you whether the server is managing its own gain automatically (like AGC — Automatic Gain Control) or whether the gain is controlled manually by the BLE client. There are four possible values organized in a 2×2 pattern:

Gain_Mode Values — 2×2 Classification
Fixed (can NOT switch) Switchable (CAN switch)
Manual Gain
(client controls gain)
0
Manual Only
Server only supports manual gain. Cannot be switched to auto.
Set Automatic Gain Mode → Error 0x84
2
Manual
Currently in manual mode. Can be switched to Automatic (value 3).
Set Automatic Gain Mode → switches to mode 3
Automatic Gain
(server controls gain)
1
Automatic Only
Server only supports automatic gain (AGC). Cannot be switched to manual.
Set Manual Gain Mode → Error 0x84
3
Automatic
Currently in auto mode. Can be switched to Manual (value 2).
Set Manual Gain Mode → switches to mode 2
When Gain_Mode is Automatic (1 or 3): the Gain_Setting field is ignored by the server — the server manages its own AGC level

💡 Important for BlueZ Developers: When you write Set Gain Setting (opcode 0x01) to the control point, the server only applies the new gain if Gain_Mode is Manual (2) or Manual Only (0). If the device is in Automatic or Automatic Only mode, the server will not change the gain — but it also will NOT return an error. The write succeeds silently. You must check the Gain_Mode first.

Field 4 — Change_Counter (Byte 3, uint8)

The Change_Counter is the most subtle field. It solves a classic TOCTOU (Time Of Check To Time Of Use) race condition in BLE. Here is the problem without Change_Counter:

The Race Condition AICS Solves — Why Change_Counter Exists
Client A (Phone) Server (Hearing Aid) Client B (Tablet)
1. Reads State: Gain=5, CC=3 1. Reads State: Gain=5, CC=3
2. Writes: SetGain(CC=3, Gain=10)
3. Applied! Gain=10, CC now=4
Notifies all clients
4. Writes: SetGain(CC=3, Gain=7)
CC=3 is STALE now!
5. Current CC=4, got CC=3
→ Returns Error 0x80!
6. Client A re-reads State
Gets: Gain=10, CC=4
Retries with CC=4
7. CC matches! Applies Gain=7
Result: Without Change_Counter, both clients could have applied conflicting writes. With CC, the second writer is forced to re-read the current state and then decides whether to still apply its change — with full knowledge of what actually happened.

Change_Counter Rollover — 255 Wraps to 0

The Change_Counter is a uint8 — maximum value 255. When the server needs to increment beyond 255, it wraps around to 0. This is intentional and well-defined. The client should handle this wrapping correctly when comparing counters.

Change_Counter Rollover — Increment Sequence
CC = 253 +1 CC = 254 +1 CC = 255 ↺+1 CC = 0
Server: change_counter = (change_counter + 1) & 0xFF;

The server initializes Change_Counter to an arbitrary value at boot — not necessarily 0. So your client code must always read the Audio Input State first before attempting any write, no matter what you think the current counter is.

Reading Audio Input State in BlueZ (C Example)

Below is a practical C example of how a BlueZ-based GATT client would read the Audio Input State characteristic and decode all four fields. This uses the BlueZ D-Bus GATT API (the correct way in modern BlueZ 5.x).

/* File: aics_client.c – BlueZ D-Bus GATT client example */ /* Assumes you have a proxy to the AudioInputState characteristic */ #include <stdint.h> #include <stdbool.h> #include <stdio.h> /* AICS Audio Input State – decoded structure */ struct aics_state { int8_t gain_setting; /* Signed: current gain step count */ uint8_t mute; /* 0=NotMuted, 1=Muted, 2=Disabled */ uint8_t gain_mode; /* 0=ManualOnly,1=AutoOnly,2=Manual,3=Auto */ uint8_t change_counter; /* Used in all control point writes */ }; /* Mute field names */ static const char *mute_names[] = { “Not Muted”, “Muted”, “Disabled” }; /* Gain mode names */ static const char *mode_names[] = { “Manual Only”, “Automatic Only”, “Manual”, “Automatic” }; /* Decode 4 raw bytes into aics_state */ static int aics_decode_state(const uint8_t *raw, size_t len, struct aics_state *out) { if (len < 4) { fprintf(stderr, “AICS: AudioInputState too short (%zu bytes)\n”, len); return -1; } out->gain_setting = (int8_t)raw[0]; /* int8 — cast! */ out->mute = raw[1]; out->gain_mode = raw[2]; out->change_counter = raw[3]; /* Validate fields */ if (out->mute > 2) { fprintf(stderr, “AICS: Invalid Mute value %u\n”, out->mute); return -1; } if (out->gain_mode > 3) { fprintf(stderr, “AICS: Invalid Gain_Mode value %u\n”, out->gain_mode); return -1; } return 0; } /* Print the decoded state */ static void aics_print_state(const struct aics_state *s, const struct gain_properties *props) { double actual_db = (double)s->gain_setting * props->units * 0.1; printf(“— Audio Input State —\n”); printf(” Gain_Setting : %d steps (%.1f dB)\n”, s->gain_setting, actual_db); printf(” Mute : %s (%u)\n”, mute_names[s->mute], s->mute); printf(” Gain_Mode : %s (%u)\n”, mode_names[s->gain_mode], s->gain_mode); printf(” Change_Counter: %u\n”, s->change_counter); } /* Usage example */ int main(void) { /* Raw 4 bytes as received over BLE ATT Read Response */ uint8_t raw_state[4] = { 0x03, 0x00, 0x02, 0x07 }; struct gain_properties props = { .units = 10, .minimum = -19, .maximum = 14 }; struct aics_state state; if (aics_decode_state(raw_state, sizeof(raw_state), &state) == 0) { aics_print_state(&state, &props); } /* Output: * — Audio Input State — * Gain_Setting : 3 steps (3.0 dB) * Mute : Not Muted (0) * Gain_Mode : Manual (2) * Change_Counter: 7 */ return 0; }

Handling Notifications — When the Server Pushes Updates

Whenever any of the three state fields (Gain_Setting, Mute, Gain_Mode) changes, the server increments the Change_Counter and sends a GATT Notification to all subscribed clients. The notification payload is the full 4-byte Audio Input State value — same format as a Read Response.

Notification Flow — Server-Side State Change
GATT Client
1. Enable notifications:
Write 0x0001 to CCCD

5. Receive notification
ATT_HANDLE_VALUE_NTF
[03][01][02][08]
(Gain=3, Muted, Manual, CC=8)

BLE ATT
GATT Server
2. Client registered for notify

3. User presses Mute button
hardware: mute = 1
change_counter = (7+1)=8

4. Server sends ATT Notify
to all subscribed clients

BLE ATT
GATT Client 2
Also receives the same notification — all clients get identical state

To receive notifications in BlueZ, you call StartNotify() on the characteristic’s D-Bus proxy, or use gatttool to write to the CCCD manually:

## Using gatttool to enable Audio Input State notifications ## (Replace 0x0012 with actual handle of Audio Input State) ## (Replace 0x0013 with actual CCCD handle) # 1. Connect and pair (must encrypt first) $ bluetoothctl [bluetooth]# connect AA:BB:CC:DD:EE:FF # 2. Discover Audio Input State CCCD handle $ gatttool -b AA:BB:CC:DD:EE:FF –char-read –uuid=0x2902 # 3. Enable notifications (write 0x0100 = little-endian for 0x0001) $ gatttool -b AA:BB:CC:DD:EE:FF –char-write-req –handle=0x0013 –value=0100 # 4. Now read the current Audio Input State $ gatttool -b AA:BB:CC:DD:EE:FF –char-read –handle=0x0012 Characteristic value/descriptor: 03 00 02 07 # ^ ^ ^ ^ # | | | Change_Counter = 7 # | | Gain_Mode = 2 (Manual) # | Mute = 0 (Not Muted) # Gain_Setting = 3 (int8)

Key Concepts in Part 3

int8 Gain_Setting Gain_Setting_Units 0.1 dB per unit Mute Disabled state Manual Only vs Manual Automatic Only vs Automatic Change_Counter anti-race TOCTOU race condition uint8 rollover at 255 CCCD 0x2902 ATT Notify

Next: Part 4 — Control Point Procedures

Part 4 covers all 5 opcodes — exact byte formats, what happens on success and failure, and how to chain multiple operations safely.

→ Part 4: Control Point Procedures

Leave a Reply

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