in 4 Bytes
Manual / Auto
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
| 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) |
0x03 0x00 0x02 0x07 means Gain=+3, NotMuted, Manual, CC=7Field 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_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 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.
| 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) |
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.
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.
|
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)
|
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:
| 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
|
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:
| 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 |
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.
| CC = 253 | +1 | CC = 254 | +1 | CC = 255 | ↺+1 | CC = 0 |
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).
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.
|
GATT Client
1. Enable notifications:
Write 0x0001 to CCCD 5. Receive notification |
⇄
BLE ATT
|
GATT Server
2. Client registered for notify
3. User presses Mute button 4. Server sends ATT Notify |
⇄
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:
Key Concepts in Part 3
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.
