summaryrefslogtreecommitdiff
path: root/src/class
diff options
context:
space:
mode:
authorHiFiPHile <[email protected]>2026-08-25 09:29:08 +0200
committerHiFiPHile <[email protected]>2026-08-25 09:29:08 +0200
commitb45f1292dd468a542197c48c3f87b471dd64a3ef (patch)
tree2e5e815476a8263fb1c3c51d24760a561db8f880 /src/class
parentd494b7c5f1fb690ea559dd3f09bb9f84c0c6d80f (diff)
feat(audio): discover stream mute and volume controls
Inspect each associated Feature Unit during mount, cache master mute support and the common MIN, MAX, and RES volume range, and ignore units with neither control. Add typed asynchronous and synchronous mute and volume APIs and demonstrate them in the host example. Signed-off-by: HiFiPHile <[email protected]>
Diffstat (limited to 'src/class')
-rw-r--r--src/class/audio/audio_host.c315
-rw-r--r--src/class/audio/audio_host.h46
2 files changed, 315 insertions, 46 deletions
diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c
index c12493a5c..2a8df9075 100644
--- a/src/class/audio/audio_host.c
+++ b/src/class/audio/audio_host.c
@@ -135,7 +135,10 @@ typedef struct {
bool running; // tuh_audio_start() called, transfers may be submitted
// One Feature Unit associated with this logical stream (0 = none)
- uint8_t feature_unit_id;
+ uint8_t feature_unit_id;
+ bool mute_supported;
+ bool volume_supported;
+ tuh_audio_volume_range_t volume_range;
// Size in bytes of one frame (all channels) of the active configuration
uint16_t frame_bytes;
@@ -173,12 +176,16 @@ typedef struct {
TUH_EPBUF_DEF(fu_ctrl, 8); // feature-unit SET data
TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); // capture transfer buffer
TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); // playback transfer buffer
- // Feature-unit request state: only one GET or SET in flight per device
- tuh_xfer_cb_t complete_cb;
- uintptr_t user_data;
- uint16_t *value;
- uint8_t width;
- bool fu_busy;
+ // Feature-unit request state: only one operation in flight per device
+ tuh_xfer_cb_t complete_cb;
+ uintptr_t user_data;
+ void *value;
+ tuh_audio_volume_range_t volume_range_temp;
+ uint8_t width;
+ uint8_t value_type;
+ uint8_t volume_stream_idx;
+ uint8_t volume_range_step;
+ bool fu_busy;
} audioh_epbuf_t;
static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX];
@@ -229,6 +236,12 @@ static tuh_audio_stream_t *audioh_get_stream_by_idx(audioh_interface_t *p_audio,
return NULL;
}
+static void audioh_stream_set_feature_unit(tuh_audio_stream_t *s, uint8_t unit_id, uint8_t controls) {
+ s->feature_unit_id = unit_id;
+ s->mute_supported = (controls & AUDIO10_FU_CONTROL_BM_MUTE) != 0;
+ s->volume_supported = (controls & AUDIO10_FU_CONTROL_BM_VOLUME) != 0;
+}
+
// Map a UAC 1.0 (subframe size, bit resolution) pair to a supported format
static bool audioh_format_from_uac1(uint8_t subframe_size, uint8_t bit_resolution, tuh_audio_format_t *format) {
if (subframe_size == 1 && bit_resolution == 8) {
@@ -282,6 +295,9 @@ static void audioh_stream_reset(tuh_audio_stream_t *s) {
s->state = STREAM_STATE_IDLE;
s->running = false;
s->feature_unit_id = 0;
+ s->mute_supported = false;
+ s->volume_supported = false;
+ s->volume_range = (tuh_audio_volume_range_t){0};
s->frame_bytes = 0;
s->frames_per_interval = 0;
s->frames_rem = 0;
@@ -811,6 +827,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface
typedef struct {
uint8_t id;
uint8_t source_id;
+ uint8_t controls;
} audioh_fu_info_t;
audioh_fu_info_t pending_fu[TUH_AUDIO_STREAM_DIRECTION_COUNT] = {0};
@@ -837,9 +854,8 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface
usb_input_terminal_id = terminal->bTerminalID;
for (uint8_t i = 0; i < TU_ARRAY_SIZE(pending_fu); i++) {
if (pending_fu[i].source_id == usb_input_terminal_id) {
- p_audio->out_stream.feature_unit_id = pending_fu[i].id;
- pending_fu[i].id = 0;
- pending_fu[i].source_id = 0;
+ audioh_stream_set_feature_unit(&p_audio->out_stream, pending_fu[i].id, pending_fu[i].controls);
+ pending_fu[i] = (audioh_fu_info_t){0};
break;
}
}
@@ -855,9 +871,8 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface
usb_output_source_id = terminal->bSourceID;
for (uint8_t i = 0; i < TU_ARRAY_SIZE(pending_fu); i++) {
if (pending_fu[i].id == usb_output_source_id) {
- p_audio->in_stream.feature_unit_id = pending_fu[i].id;
- pending_fu[i].id = 0;
- pending_fu[i].source_id = 0;
+ audioh_stream_set_feature_unit(&p_audio->in_stream, pending_fu[i].id, pending_fu[i].controls);
+ pending_fu[i] = (audioh_fu_info_t){0};
break;
}
}
@@ -865,18 +880,28 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface
break;
}
case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: {
- if (!TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 5)) {
+ if (!TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 7)) {
+ goto open_failed;
+ }
+ const uint8_t control_size = p_desc[5];
+ if (!TUH_VALIDATE_BASIC(control_size > 0) ||
+ !TUH_VALIDATE_BASIC(control_size <= (uint8_t)(tu_desc_len(p_desc) - 7))) {
goto open_failed;
}
+ const uint8_t controls = p_desc[6] & (AUDIO10_FU_CONTROL_BM_MUTE | AUDIO10_FU_CONTROL_BM_VOLUME);
+ if (controls == 0) {
+ break;
+ }
+
bool mapped = false;
if (usb_input_terminal_id != 0 && p_audio->out_stream.feature_unit_id == 0 &&
p_desc[4] == usb_input_terminal_id) {
- p_audio->out_stream.feature_unit_id = p_desc[3];
+ audioh_stream_set_feature_unit(&p_audio->out_stream, p_desc[3], controls);
mapped = true;
}
if (usb_output_source_id != 0 && p_audio->in_stream.feature_unit_id == 0 &&
p_desc[3] == usb_output_source_id) {
- p_audio->in_stream.feature_unit_id = p_desc[3];
+ audioh_stream_set_feature_unit(&p_audio->in_stream, p_desc[3], controls);
mapped = true;
}
if (!mapped) {
@@ -884,6 +909,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface
if (pending_fu[i].id == 0) {
pending_fu[i].id = p_desc[3];
pending_fu[i].source_id = p_desc[4];
+ pending_fu[i].controls = controls;
break;
}
}
@@ -956,6 +982,8 @@ open_failed:
//--------------------------------------------------------------------+
// Set Configuration
//--------------------------------------------------------------------+
+static void audioh_mount_feature_unit_next(uint8_t idx);
+
bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) {
uint8_t idx = TUSB_INDEX_INVALID_8;
for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) {
@@ -972,13 +1000,9 @@ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) {
return true;
}
- audioh_interface_t *p_audio = &_audioh_itf[idx];
- p_audio->mounted = true;
- TU_LOG_DRV(" AUDIO mounted: addr = %u index = %u\r\n", dev_addr, idx);
-
- tuh_audio_mount_cb(idx);
-
- usbh_driver_set_config_complete(dev_addr, itf_num);
+ audioh_epbuf_t *epbuf = &_audioh_epbuf[idx];
+ epbuf->volume_stream_idx = 0;
+ audioh_mount_feature_unit_next(idx);
return true;
}
@@ -1005,6 +1029,24 @@ uint8_t tuh_audio_get_feature_unit_id(uint8_t idx, uint8_t stream_idx) {
return s->feature_unit_id;
}
+bool tuh_audio_mute_supported(uint8_t idx, uint8_t stream_idx) {
+ TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false);
+ audioh_interface_t *p_audio = &_audioh_itf[idx];
+ TU_VERIFY(p_audio->mounted, false);
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx);
+ return s != NULL && s->mute_supported;
+}
+
+bool tuh_audio_volume_range_get(uint8_t idx, uint8_t stream_idx, tuh_audio_volume_range_t *range) {
+ TU_VERIFY(idx < CFG_TUH_AUDIO_MAX && range != NULL, false);
+ audioh_interface_t *p_audio = &_audioh_itf[idx];
+ TU_VERIFY(p_audio->mounted, false);
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx);
+ TU_VERIFY(s != NULL && s->volume_supported, false);
+ *range = s->volume_range;
+ return true;
+}
+
uint8_t tuh_audio_stream_count(uint8_t dev_idx) {
TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0);
audioh_interface_t *p_audio = &_audioh_itf[dev_idx];
@@ -1300,21 +1342,42 @@ static void audioh_fu_set_complete(tuh_xfer_t *xfer) {
}
}
+enum {
+ AUDIOH_FU_VALUE_U16,
+ AUDIOH_FU_VALUE_BOOL,
+ AUDIOH_FU_VALUE_I16
+};
+
+static void audioh_fu_value_store(audioh_epbuf_t *epbuf) {
+ if (epbuf->value_type == AUDIOH_FU_VALUE_BOOL) {
+ *((bool *)epbuf->value) = epbuf->fu_ctrl[0] != 0;
+ } else if (epbuf->width == 1) {
+ *((uint16_t *)epbuf->value) = epbuf->fu_ctrl[0];
+ } else {
+ const uint16_t value = tu_le16toh(tu_unaligned_read16(epbuf->fu_ctrl));
+ if (epbuf->value_type == AUDIOH_FU_VALUE_I16) {
+ *((int16_t *)epbuf->value) = (int16_t)value;
+ } else {
+ *((uint16_t *)epbuf->value) = value;
+ }
+ }
+}
+
// Convert the raw control value to host order and chain to the application callback
static void audioh_fu_get_complete(tuh_xfer_t *xfer) {
const uint8_t idx = (uint8_t)xfer->user_data;
audioh_epbuf_t *epbuf = &_audioh_epbuf[idx];
tuh_xfer_cb_t app_cb = epbuf->complete_cb;
uintptr_t user_data = epbuf->user_data;
- uint16_t *value = epbuf->value;
- const uint8_t width = epbuf->width;
epbuf->complete_cb = NULL;
epbuf->fu_busy = false;
- if (app_cb != NULL && value != NULL && xfer->result == XFER_RESULT_SUCCESS) {
- const uint8_t *raw = (const uint8_t *)value;
- // The raw bytes are little-endian on the wire: rebuild the host-order value
- *value = (width == 1) ? (uint16_t)raw[0] : (uint16_t)((uint16_t)raw[0] | ((uint16_t)raw[1] << 8));
+ if (epbuf->value != NULL && xfer->result == XFER_RESULT_SUCCESS) {
+ if (xfer->actual_len == epbuf->width) {
+ audioh_fu_value_store(epbuf);
+ } else {
+ xfer->result = XFER_RESULT_FAILED;
+ }
}
xfer->user_data = user_data;
@@ -1323,6 +1386,133 @@ static void audioh_fu_get_complete(tuh_xfer_t *xfer) {
}
}
+enum {
+ AUDIOH_VOLUME_RANGE_MIN,
+ AUDIOH_VOLUME_RANGE_MAX,
+ AUDIOH_VOLUME_RANGE_RES,
+ AUDIOH_VOLUME_RANGE_COUNT
+};
+
+static uint8_t audioh_fu_volume_range_request(uint8_t step) {
+ switch (step) {
+ case AUDIOH_VOLUME_RANGE_MIN:
+ return AUDIO10_CS_REQ_GET_MIN;
+ case AUDIOH_VOLUME_RANGE_MAX:
+ return AUDIO10_CS_REQ_GET_MAX;
+ case AUDIOH_VOLUME_RANGE_RES:
+ return AUDIO10_CS_REQ_GET_RES;
+ default:
+ return AUDIO10_CS_REQ_UNDEF;
+ }
+}
+
+static void audioh_fu_volume_range_store(audioh_epbuf_t *epbuf) {
+ const uint16_t value = tu_le16toh(tu_unaligned_read16(epbuf->fu_ctrl));
+ switch (epbuf->volume_range_step) {
+ case AUDIOH_VOLUME_RANGE_MIN:
+ epbuf->volume_range_temp.min = (int16_t)value;
+ break;
+ case AUDIOH_VOLUME_RANGE_MAX:
+ epbuf->volume_range_temp.max = (int16_t)value;
+ break;
+ case AUDIOH_VOLUME_RANGE_RES:
+ epbuf->volume_range_temp.res = value;
+ break;
+ default:
+ break;
+ }
+}
+
+static void audioh_mount_feature_unit_complete(tuh_xfer_t *xfer);
+
+static bool audioh_mount_feature_unit_submit(uint8_t idx) {
+ audioh_interface_t *p_audio = &_audioh_itf[idx];
+ audioh_epbuf_t *epbuf = &_audioh_epbuf[idx];
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, epbuf->volume_stream_idx);
+ TU_ASSERT(s != NULL);
+
+ const tusb_control_request_t request = {
+ .bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN},
+ .bRequest = audioh_fu_volume_range_request(epbuf->volume_range_step),
+ .wValue = tu_htole16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 0)),
+ .wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)),
+ .wLength = tu_htole16(2),
+ };
+ tuh_xfer_t xfer = {.daddr = p_audio->daddr,
+ .ep_addr = 0,
+ .setup = &request,
+ .buffer = epbuf->fu_ctrl,
+ .complete_cb = audioh_mount_feature_unit_complete,
+ .user_data = (uintptr_t)idx};
+ return tuh_control_xfer(&xfer);
+}
+
+static void audioh_mount_feature_unit_next(uint8_t idx) {
+ audioh_interface_t *p_audio = &_audioh_itf[idx];
+ audioh_epbuf_t *epbuf = &_audioh_epbuf[idx];
+
+ while (epbuf->volume_stream_idx < p_audio->stream_count) {
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, epbuf->volume_stream_idx);
+ TU_ASSERT(s != NULL, );
+ if (s->volume_supported) {
+ epbuf->volume_range_temp = (tuh_audio_volume_range_t){0};
+ epbuf->volume_range_step = AUDIOH_VOLUME_RANGE_MIN;
+ epbuf->fu_busy = true;
+ if (audioh_mount_feature_unit_submit(idx)) {
+ return;
+ }
+ s->volume_supported = false;
+ if (!s->mute_supported) {
+ s->feature_unit_id = 0;
+ }
+ epbuf->fu_busy = false;
+ }
+ epbuf->volume_stream_idx++;
+ }
+
+ p_audio->mounted = true;
+ TU_LOG_DRV(" AUDIO mounted: addr = %u index = %u\r\n", p_audio->daddr, idx);
+ tuh_audio_mount_cb(idx);
+ usbh_driver_set_config_complete(p_audio->daddr, p_audio->ac_itf_num);
+}
+
+static void audioh_mount_feature_unit_complete(tuh_xfer_t *xfer) {
+ const uint8_t idx = (uint8_t)xfer->user_data;
+ audioh_epbuf_t *epbuf = &_audioh_epbuf[idx];
+ if (!epbuf->fu_busy) {
+ return;
+ }
+ audioh_interface_t *p_audio = &_audioh_itf[idx];
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, epbuf->volume_stream_idx);
+ TU_ASSERT(s != NULL, );
+
+ if (xfer->result == XFER_RESULT_SUCCESS && xfer->actual_len == 2) {
+ audioh_fu_volume_range_store(epbuf);
+ epbuf->volume_range_step++;
+
+ if (epbuf->volume_range_step < AUDIOH_VOLUME_RANGE_COUNT) {
+ if (audioh_mount_feature_unit_submit(idx)) {
+ return;
+ }
+ xfer->result = XFER_RESULT_FAILED;
+ } else if (epbuf->volume_range_temp.min <= epbuf->volume_range_temp.max && epbuf->volume_range_temp.res > 0) {
+ s->volume_range = epbuf->volume_range_temp;
+ } else {
+ xfer->result = XFER_RESULT_FAILED;
+ }
+ }
+
+ if (xfer->result != XFER_RESULT_SUCCESS || xfer->actual_len != 2) {
+ s->volume_supported = false;
+ if (!s->mute_supported) {
+ s->feature_unit_id = 0;
+ }
+ }
+ epbuf->fu_busy = false;
+ epbuf->volume_stream_idx++;
+ audioh_mount_feature_unit_next(idx);
+}
+
bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel,
uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) {
TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false);
@@ -1378,8 +1568,8 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control
return true;
}
-bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel,
- uint16_t *value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) {
+static bool audioh_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel,
+ void *value, uint8_t value_type, tuh_xfer_cb_t complete_cb, uintptr_t user_data) {
TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false);
audioh_interface_t *p_audio = &_audioh_itf[idx];
TU_VERIFY(p_audio->mounted && value, false);
@@ -1391,7 +1581,10 @@ bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control
audioh_epbuf_t *epbuf = &_audioh_epbuf[idx];
TU_VERIFY(!epbuf->fu_busy, false);
- epbuf->fu_busy = true;
+ epbuf->fu_busy = true;
+ epbuf->value = value;
+ epbuf->width = width;
+ epbuf->value_type = value_type;
const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE,
.type = TUSB_REQ_TYPE_CLASS,
@@ -1407,16 +1600,17 @@ bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control
tuh_xfer_t xfer = {.daddr = p_audio->daddr,
.ep_addr = 0,
.setup = &request,
- .buffer = (uint8_t *)value,
+ .buffer = epbuf->fu_ctrl,
.complete_cb = NULL,
.user_data = user_data};
if (!tuh_control_xfer(&xfer)) {
epbuf->fu_busy = false;
return false;
}
- if (xfer.result == XFER_RESULT_SUCCESS) {
- const uint8_t *raw = (const uint8_t *)value;
- *value = (width == 1) ? (uint16_t)raw[0] : (uint16_t)((uint16_t)raw[0] | ((uint16_t)raw[1] << 8));
+ if (xfer.result == XFER_RESULT_SUCCESS && xfer.actual_len == width) {
+ audioh_fu_value_store(epbuf);
+ } else if (xfer.result == XFER_RESULT_SUCCESS && user_data != 0) {
+ *((tusb_xfer_result_t *)user_data) = XFER_RESULT_FAILED;
}
epbuf->fu_busy = false;
return true;
@@ -1425,15 +1619,12 @@ bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control
// Async path: chain the host-order conversion to the application callback
epbuf->complete_cb = complete_cb;
epbuf->user_data = user_data;
- epbuf->value = value;
- epbuf->width = width;
-
- tuh_xfer_t xfer = {.daddr = p_audio->daddr,
- .ep_addr = 0,
- .setup = &request,
- .buffer = (uint8_t *)value, // raw bytes, converted in audioh_fu_get_complete()
- .complete_cb = audioh_fu_get_complete,
- .user_data = (uintptr_t)idx};
+ tuh_xfer_t xfer = {.daddr = p_audio->daddr,
+ .ep_addr = 0,
+ .setup = &request,
+ .buffer = epbuf->fu_ctrl,
+ .complete_cb = audioh_fu_get_complete,
+ .user_data = (uintptr_t)idx};
if (!tuh_control_xfer(&xfer)) {
epbuf->complete_cb = NULL;
@@ -1443,4 +1634,38 @@ bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control
return true;
}
+bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel,
+ uint16_t *value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) {
+ return audioh_feature_unit_get(idx, stream_idx, control_selector, channel, value, AUDIOH_FU_VALUE_U16, complete_cb,
+ user_data);
+}
+
+bool tuh_audio_mute_set(uint8_t idx, uint8_t stream_idx, bool mute, tuh_xfer_cb_t complete_cb, uintptr_t user_data) {
+ TU_VERIFY(tuh_audio_mute_supported(idx, stream_idx), false);
+ return tuh_audio_feature_unit_set(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, 0, mute ? 1 : 0, complete_cb, user_data);
+}
+
+bool tuh_audio_mute_get(uint8_t idx, uint8_t stream_idx, bool *mute, tuh_xfer_cb_t complete_cb, uintptr_t user_data) {
+ TU_VERIFY(mute != NULL && tuh_audio_mute_supported(idx, stream_idx), false);
+ return audioh_feature_unit_get(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, 0, mute, AUDIOH_FU_VALUE_BOOL, complete_cb,
+ user_data);
+}
+
+bool tuh_audio_volume_set(uint8_t idx, uint8_t stream_idx, int16_t volume, tuh_xfer_cb_t complete_cb,
+ uintptr_t user_data) {
+ tuh_audio_volume_range_t range;
+ TU_VERIFY(tuh_audio_volume_range_get(idx, stream_idx, &range), false);
+ TU_VERIFY(volume >= range.min && volume <= range.max, false);
+ return tuh_audio_feature_unit_set(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, (uint16_t)volume, complete_cb,
+ user_data);
+}
+
+bool tuh_audio_volume_get(uint8_t idx, uint8_t stream_idx, int16_t *volume, tuh_xfer_cb_t complete_cb,
+ uintptr_t user_data) {
+ tuh_audio_volume_range_t range;
+ TU_VERIFY(volume != NULL && tuh_audio_volume_range_get(idx, stream_idx, &range), false);
+ return audioh_feature_unit_get(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, volume, AUDIOH_FU_VALUE_I16, complete_cb,
+ user_data);
+}
+
#endif
diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h
index 98e103208..e850e4b7c 100644
--- a/src/class/audio/audio_host.h
+++ b/src/class/audio/audio_host.h
@@ -85,6 +85,17 @@ typedef struct {
uint8_t channels;
} tuh_audio_stream_config_t;
+// Volume values are signed 1/256 dB. INT16_MIN represents silence.
+#define TUH_AUDIO_VOLUME_SILENCE INT16_MIN
+
+// One continuous volume range. This matches UAC1 MIN/MAX/RES and the common
+// UAC2 RANGE response containing one subrange.
+typedef struct {
+ int16_t min;
+ int16_t max;
+ uint16_t res;
+} tuh_audio_volume_range_t;
+
//--------------------------------------------------------------------+
// Stream Enumeration
//--------------------------------------------------------------------+
@@ -183,6 +194,10 @@ bool tuh_audio_mounted(uint8_t idx);
uint8_t tuh_audio_get_dev_addr(uint8_t idx);
// Get the Feature Unit ID associated with a stream (0 = none)
uint8_t tuh_audio_get_feature_unit_id(uint8_t idx, uint8_t stream_idx);
+// True when the stream's Feature Unit supports master mute control.
+bool tuh_audio_mute_supported(uint8_t idx, uint8_t stream_idx);
+// Get the cached master volume range. Returns false when volume is unsupported.
+bool tuh_audio_volume_range_get(uint8_t idx, uint8_t stream_idx, tuh_audio_volume_range_t *range);
//--------------------------------------------------------------------+
// Control Request API
@@ -197,10 +212,19 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control
// Get a Feature Unit control (mute, volume, ...) associated with an Audio stream (UAC 1.0)
// The value is converted to host byte order before complete_cb is invoked.
// Graphic EQ and unknown selectors are unsupported.
-// Only one Feature Unit GET or SET request may be in flight per device.
+// Only one Feature Unit operation may be in flight per device.
bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel,
uint16_t *value, tuh_xfer_cb_t complete_cb, uintptr_t user_data);
+// Master mute and volume controls. Capability and range information is cached
+// before tuh_audio_mount_cb() is invoked.
+bool tuh_audio_mute_set(uint8_t idx, uint8_t stream_idx, bool mute, tuh_xfer_cb_t complete_cb, uintptr_t user_data);
+bool tuh_audio_mute_get(uint8_t idx, uint8_t stream_idx, bool *mute, tuh_xfer_cb_t complete_cb, uintptr_t user_data);
+bool tuh_audio_volume_set(uint8_t idx, uint8_t stream_idx, int16_t volume, tuh_xfer_cb_t complete_cb,
+ uintptr_t user_data);
+bool tuh_audio_volume_get(uint8_t idx, uint8_t stream_idx, int16_t *volume, tuh_xfer_cb_t complete_cb,
+ uintptr_t user_data);
+
//--------------------------------------------------------------------+
// Control Request Sync API
// Each Function will make a USB control transfer request to/from device the function will block until request is
@@ -220,6 +244,26 @@ TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_feature_unit_ge
TU_API_SYNC(tuh_audio_feature_unit_get, idx, stream_idx, control_selector, channel, value);
}
+TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_mute_set_sync(uint8_t idx, uint8_t stream_idx,
+ bool mute) {
+ TU_API_SYNC(tuh_audio_mute_set, idx, stream_idx, mute);
+}
+
+TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_mute_get_sync(uint8_t idx, uint8_t stream_idx,
+ bool *mute) {
+ TU_API_SYNC(tuh_audio_mute_get, idx, stream_idx, mute);
+}
+
+TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_volume_set_sync(uint8_t idx, uint8_t stream_idx,
+ int16_t volume) {
+ TU_API_SYNC(tuh_audio_volume_set, idx, stream_idx, volume);
+}
+
+TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_volume_get_sync(uint8_t idx, uint8_t stream_idx,
+ int16_t *volume) {
+ TU_API_SYNC(tuh_audio_volume_get, idx, stream_idx, volume);
+}
+
//--------------------------------------------------------------------+
// Callbacks (Weak is optional)
//--------------------------------------------------------------------+