summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiFiPHile <[email protected]>2026-09-02 15:22:07 +0200
committerHiFiPHile <[email protected]>2026-09-02 15:24:36 +0200
commit1ff684a07ec5d80a89ba82276139a202288b7716 (patch)
tree96aab26ac287e2e2e976b1187baa050338f25b45
parent93545d0bfb90b48a33a08b8b12df21a886b7a338 (diff)
implement per channel volume control
Signed-off-by: HiFiPHile <[email protected]>
-rw-r--r--examples/host/audio_host/README.md16
-rw-r--r--examples/host/audio_host/src/audio_app.c26
-rw-r--r--hw/bsp/stm32f7/boards/stm32f723disco/board.h6
-rw-r--r--src/class/audio/audio_host.c313
-rw-r--r--src/class/audio/audio_host.h33
-rw-r--r--test/unit-test/test/host/audio/test_audio_host.c141
6 files changed, 391 insertions, 144 deletions
diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md
index 3d4307b42..7f72aabaf 100644
--- a/examples/host/audio_host/README.md
+++ b/examples/host/audio_host/README.md
@@ -6,7 +6,7 @@ This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO)
- Enumerates and mounts USB Audio Class 1.0 and 2.0 devices
- Discovers the device's logical streams (capture/playback) and their supported configurations (discrete tuples only)
-- Reports each stream's master mute/volume capabilities and cached volume range
+- Reports each stream's mute/volume capabilities and cached volume range
- Configures and starts an S16_LE capture stream (44.1 kHz preferred, 48 kHz fallback; stereo preferred, mono accepted)
- Echoes captured audio to an S16_LE playback stream at the same sample rate (same channel count preferred, mono/stereo conversion otherwise)
- Frame-based FIFO API: the main loop reads capture when its FIFO is half full and fills playback when its FIFO is half drained; USB transfer callbacks are not used for FIFO servicing
@@ -28,7 +28,7 @@ The echo needs a matching S16_LE playback stream at the capture sample rate; dev
- UAC1 Type I Format descriptors with `bSamFreqType == 0` are unsupported; the driver requires a list of discrete sampling frequencies.
- UAC2 supports direct Clock Sources. Clock Selectors, Clock Multipliers, Sampling Rate Converters, Clock Validity, and Valid Alternate Settings controls are not handled.
- UAC2 sampling-frequency RANGE responses are expanded into at most `CFG_TUH_AUDIO_MAX_SAM_FREQ` discrete configurations. A read-only Clock Source exposes only its current frequency.
-- Master mute and volume controls are discovered before the mount callback, including the volume range. Feature Units without master mute or volume are ignored. UAC2 volume discovery supports the common RANGE response containing one subrange.
+- Master mute and volume controls are discovered before the mount callback. A Feature Unit with volume only on its logical channels is also supported: the range is read from the first controlled channel, and a stream-volume SET writes every logical channel when no writable master control exists. The typed API assumes all logical channels share one range; applications needing different per-channel ranges can use the raw control API. UAC2 volume discovery supports the common RANGE response containing one subrange.
- The `MaxPacketsOnly` endpoint attribute is not supported. OUT transfers are not padded to `wMaxPacketSize`, and padding in IN transfers is not removed from the reported audio data.
## Building
@@ -68,10 +68,10 @@ make BOARD=<your_board> flash
2. Connect a USB Audio device (UAC 1.0 or 2.0) to the USB host port
3. Open a serial terminal to view output
4. The example will:
- - Print each stream's master mute/volume capabilities, cached volume range, and supported configurations when mounted
+ - Print each stream's mute/volume capabilities, cached volume range, and supported configurations when mounted
- Look for an S16_LE capture configuration at a preferred sample rate (44.1 kHz first, 48 kHz fallback; stereo preferred, mono accepted) and configure it
- Echo captured audio to an S16_LE playback configuration at the same sample rate (same channel count preferred, converted otherwise)
- - Read/unmute the microphone and speaker Feature Units and set supported master volumes near -6 dB
+ - Read/unmute the microphone and speaker Feature Units and set supported stream volumes near -6 dB; a channel-only Feature Unit is updated one logical channel at a time
- Service both FIFOs from `audio_app_task()` at their half-full/half-drained watermarks; a sine test tone plays on the playback stream when no capture stream is echoing
- Cycle through the three phases (mic-only / spk-only / echo, 5 s each) with `tuh_audio_start()` / `tuh_audio_stop()`; their asynchronous results are printed from `tuh_audio_event_cb()`, and a failed stream is restarted automatically after 100 ms
@@ -83,24 +83,24 @@ Connect a USB Audio Device (UAC 1.0 or 2.0) to test
Audio device mounted: idx=0 addr=1
capture stream 1, configurations: 2
master mute supported
- master volume range: min=-23040 max=1536 res=256 (1/256 dB)
+ volume range: min=-23040 max=1536 res=256 (1/256 dB)
[0] format=1 rate=44100 channels=2
[1] format=1 rate=48000 channels=2
playback stream 0, configurations: 2
master mute supported
- master volume range: min=-23040 max=1536 res=256 (1/256 dB)
+ volume range: min=-23040 max=1536 res=256 (1/256 dB)
[0] format=1 rate=44100 channels=2
[1] format=1 rate=48000 channels=2
Configuring 44100 S16_LE capture (2 channels)
Microphone configured
Microphone master mute: off
Microphone master volume: 0 (1/256 dB)
- Microphone master volume set: -1536 (1/256 dB)
+ Microphone volume set: -1536 (1/256 dB)
Configuring 44100 S16_LE playback (2 channels)
Speaker configured
Speaker master mute: off
Speaker master volume: 0 (1/256 dB)
- Speaker master volume set: -1536 (1/256 dB)
+ Speaker volume set: -1536 (1/256 dB)
```
## Configuration
diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c
index 55efe3295..749274f44 100644
--- a/examples/host/audio_host/src/audio_app.c
+++ b/examples/host/audio_host/src/audio_app.c
@@ -365,7 +365,7 @@ static void print_stream_configs(uint8_t idx, uint8_t stream_idx) {
printf(" master mute supported\r\n");
}
if (tuh_audio_volume_range_get(idx, stream_idx, &range)) {
- printf(" master volume range: min=%d max=%d res=%u (1/256 dB)\r\n", (int)range.min, (int)range.max,
+ printf(" volume range: min=%d max=%d res=%u (1/256 dB)\r\n", (int)range.min, (int)range.max,
(unsigned)range.res);
}
for (uint8_t i = 0; i < tuh_audio_config_count(idx, stream_idx); i++) {
@@ -397,26 +397,26 @@ static void configure_stream_controls(uint8_t idx, uint8_t stream_idx, const cha
if (tuh_audio_volume_range_get(idx, stream_idx, &range)) {
has_control = true;
int16_t volume;
- tusb_xfer_result_t result = tuh_audio_volume_get_sync(idx, stream_idx, &volume);
+ tusb_xfer_result_t result = tuh_audio_volume_get_sync(idx, stream_idx, 0, &volume);
if (result == XFER_RESULT_SUCCESS) {
printf(" %s master volume: %d (1/256 dB)\r\n", stream_name, volume);
- int32_t target = FEATURE_UNIT_VOLUME_DB;
- target = TU_MAX(target, range.min);
- target = TU_MIN(target, range.max);
- target = range.min + ((target - range.min + range.res / 2) / range.res) * range.res;
- target = TU_MIN(target, range.max);
- result = tuh_audio_volume_set_sync(idx, stream_idx, (int16_t)target);
- if (result == XFER_RESULT_SUCCESS) {
- printf(" %s master volume set: %d (1/256 dB)\r\n", stream_name, (int)target);
- }
+ }
+ int32_t target = FEATURE_UNIT_VOLUME_DB;
+ target = TU_MAX(target, range.min);
+ target = TU_MIN(target, range.max);
+ target = range.min + ((target - range.min + range.res / 2) / range.res) * range.res;
+ target = TU_MIN(target, range.max);
+ result = tuh_audio_volume_set_sync(idx, stream_idx, 0, (int16_t)target);
+ if (result == XFER_RESULT_SUCCESS) {
+ printf(" %s volume set: %d (1/256 dB)\r\n", stream_name, (int)target);
}
if (result != XFER_RESULT_SUCCESS) {
- printf(" Accessing %s master volume failed: result=%u\r\n", stream_name, result);
+ printf(" Setting %s volume failed: result=%u\r\n", stream_name, result);
}
}
if (!has_control) {
- printf(" %s stream has no master mute/volume control\r\n", stream_name);
+ printf(" %s stream has no mute/volume control\r\n", stream_name);
}
}
diff --git a/hw/bsp/stm32f7/boards/stm32f723disco/board.h b/hw/bsp/stm32f7/boards/stm32f723disco/board.h
index 698a1a230..78942b884 100644
--- a/hw/bsp/stm32f7/boards/stm32f723disco/board.h
+++ b/hw/bsp/stm32f7/boards/stm32f723disco/board.h
@@ -103,9 +103,9 @@ static inline void board_clock_init(void) {
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = HSE_VALUE / 1000000;
- RCC_OscInitStruct.PLL.PLLN = 432;
- RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
- RCC_OscInitStruct.PLL.PLLQ = 9;
+ RCC_OscInitStruct.PLL.PLLN = 288;
+ RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV4;
+ RCC_OscInitStruct.PLL.PLLQ = 6;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
/* Activate the OverDrive to reach the 216 MHz Frequency */
diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c
index 5ad3ed960..d93ff535d 100644
--- a/src/class/audio/audio_host.c
+++ b/src/class/audio/audio_host.c
@@ -46,9 +46,10 @@
*
* UAC1 rates come from each Format Type descriptor, whereas UAC2 rates are
* queried from the Clock Sources referenced by the parsed topology. Feature
- * Unit parsing records master mute access, and mount probing reads the master
- * volume range. A device is reported as mounted only after these asynchronous
- * probes finish.
+ * Unit parsing records master mute and master/logical-channel volume access.
+ * Mount probing reads the volume range from the master or first controlled
+ * logical channel. A device is reported as mounted only after these
+ * asynchronous probes finish.
*
* Stream configuration is local; stream activation is asynchronous:
*
@@ -198,6 +199,8 @@ typedef struct {
struct {
uint8_t width;
uint8_t value_type;
+ uint8_t channel;
+ uint8_t last_channel;
} control;
struct {
uint8_t stream_idx;
@@ -239,7 +242,10 @@ typedef struct {
// Directly associated Feature Unit, or zero when none is usable.
uint8_t feature_unit_id;
uint8_t mute_access;
- uint8_t volume_access;
+ uint8_t volume_master_access;
+ uint8_t feature_unit_channels;
+ uint8_t volume_range_channel;
+ bool volume_all_channels_writable;
tuh_audio_volume_range_t volume_range;
// Bytes in one interleaved audio frame across all channels.
@@ -453,10 +459,14 @@ static bool audioh_stream_config_get(const tuh_audio_stream_t *s, uint8_t config
}
static void audioh_stream_set_feature_unit(tuh_audio_stream_t *s, uint8_t unit_id, uint8_t mute_access,
- uint8_t volume_access) {
- s->feature_unit_id = unit_id;
- s->mute_access = mute_access;
- s->volume_access = volume_access;
+ uint8_t volume_master_access, uint8_t channels, uint8_t volume_range_channel,
+ bool volume_all_channels_writable) {
+ s->feature_unit_id = unit_id;
+ s->mute_access = mute_access;
+ s->volume_master_access = volume_master_access;
+ s->feature_unit_channels = channels;
+ s->volume_range_channel = volume_range_channel;
+ s->volume_all_channels_writable = volume_all_channels_writable;
}
static bool audioh_format_from_pcm(uint8_t subslot_size, uint8_t bit_resolution, tuh_audio_format_t *format) {
@@ -492,20 +502,23 @@ static uint32_t audioh_nominal_frames_q16(uint32_t sample_rate, uint8_t ep_inter
// Preserve the stream identity and FIFO allocation while clearing device state.
static void audioh_stream_reset(tuh_audio_stream_t *s) {
- s->daddr = 0;
- s->stream_idx = TUSB_INDEX_INVALID_8;
- s->as_count = 0;
- s->config_count = 0;
- s->active_config = TUSB_INDEX_INVALID_8;
- s->active_as = TUSB_INDEX_INVALID_8;
- s->active_rate = TUSB_INDEX_INVALID_8;
- s->state = STREAM_STATE_IDLE;
- s->running = false;
- s->feature_unit_id = 0;
- s->mute_access = AUDIOH_CTRL_NONE;
- s->volume_access = AUDIOH_CTRL_NONE;
- s->volume_range = (tuh_audio_volume_range_t){0};
- s->frame_bytes = 0;
+ s->daddr = 0;
+ s->stream_idx = TUSB_INDEX_INVALID_8;
+ s->as_count = 0;
+ s->config_count = 0;
+ s->active_config = TUSB_INDEX_INVALID_8;
+ s->active_as = TUSB_INDEX_INVALID_8;
+ s->active_rate = TUSB_INDEX_INVALID_8;
+ s->state = STREAM_STATE_IDLE;
+ s->running = false;
+ s->feature_unit_id = 0;
+ s->mute_access = AUDIOH_CTRL_NONE;
+ s->volume_master_access = AUDIOH_CTRL_NONE;
+ s->feature_unit_channels = 0;
+ s->volume_range_channel = TUSB_INDEX_INVALID_8;
+ s->volume_all_channels_writable = false;
+ s->volume_range = (tuh_audio_volume_range_t){0};
+ s->frame_bytes = 0;
tu_edpt_stream_close(&s->edpt);
tu_edpt_stream_clear(&s->edpt);
}
@@ -905,7 +918,10 @@ typedef struct {
uint8_t id;
uint8_t source_id;
uint8_t mute_access;
- uint8_t volume_access;
+ uint8_t volume_master_access;
+ uint8_t channels;
+ uint8_t volume_range_channel;
+ bool volume_all_channels_writable;
} audioh_fu_info_t;
typedef struct {
@@ -949,8 +965,9 @@ static bool audioh_ac_entity_valid(const audioh_interface_t *p_audio, const uint
case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: {
TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 7), false);
const uint8_t control_size = p_desc[5];
- return TUH_VALIDATE_BASIC(control_size > 0) &&
- TUH_VALIDATE_BASIC(control_size <= (uint8_t)(tu_desc_len(p_desc) - 7));
+ const uint8_t control_bytes = (uint8_t)(tu_desc_len(p_desc) - 7);
+ return TUH_VALIDATE_BASIC(control_size > 0) && TUH_VALIDATE_BASIC(control_size <= control_bytes) &&
+ TUH_VALIDATE_BASIC(control_bytes % control_size == 0);
}
default:
return true;
@@ -965,7 +982,8 @@ static bool audioh_ac_entity_valid(const audioh_interface_t *p_audio, const uint
case AUDIO20_CS_AC_INTERFACE_OUTPUT_TERMINAL:
return TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio20_desc_output_terminal_t));
case AUDIO20_CS_AC_INTERFACE_FEATURE_UNIT:
- return TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 10);
+ return TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 10) &&
+ TUH_VALIDATE_BASIC((tu_desc_len(p_desc) - 6u) % 4u == 0);
case AUDIO20_CS_AC_INTERFACE_CLOCK_SOURCE:
return TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio20_desc_clock_source_t));
default:
@@ -1033,39 +1051,72 @@ static bool audioh_ac_feature_unit_parse(const audioh_interface_t *p_audio, cons
if (tu_desc_type(p_desc) != TUSB_DESC_CS_INTERFACE) {
return false;
}
+
+ uint8_t control_offset;
+ uint8_t control_size;
+ uint8_t channels;
+ uint8_t mute_access;
switch (p_audio->protocol) {
#if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1
case AUDIO_INT_PROTOCOL_CODE_V1:
- if (tu_desc_subtype(p_desc) == AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT) {
- const uint8_t controls = p_desc[6];
- *info = (audioh_fu_info_t){.id = p_desc[3],
- .source_id = p_desc[4],
- .mute_access = (controls & AUDIO10_FU_CONTROL_BM_MUTE) ? AUDIOH_CTRL_READ_WRITE
- : AUDIOH_CTRL_NONE,
- .volume_access = (controls & AUDIO10_FU_CONTROL_BM_VOLUME) ? AUDIOH_CTRL_READ_WRITE
- : AUDIOH_CTRL_NONE};
- return info->mute_access != AUDIOH_CTRL_NONE || info->volume_access != AUDIOH_CTRL_NONE;
+ if (tu_desc_subtype(p_desc) != AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT) {
+ return false;
}
+ control_offset = 6;
+ control_size = p_desc[5];
+ channels = (uint8_t)((tu_desc_len(p_desc) - 7u) / control_size - 1u);
+ mute_access = (p_desc[control_offset] & AUDIO10_FU_CONTROL_BM_MUTE) ? AUDIOH_CTRL_READ_WRITE : AUDIOH_CTRL_NONE;
break;
#endif
#if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2
case AUDIO_INT_PROTOCOL_CODE_V2:
- if (tu_desc_subtype(p_desc) == AUDIO20_CS_AC_INTERFACE_FEATURE_UNIT) {
- const uint32_t controls = tu_le32toh(tu_unaligned_read32(&p_desc[5]));
- *info =
- (audioh_fu_info_t){.id = p_desc[3],
- .source_id = p_desc[4],
- .mute_access = audioh_uac2_control_access(controls, AUDIO20_FEATURE_UNIT_CTRL_MUTE_POS),
- .volume_access =
- audioh_uac2_control_access(controls, AUDIO20_FEATURE_UNIT_CTRL_VOLUME_POS)};
- return info->mute_access != AUDIOH_CTRL_NONE || info->volume_access != AUDIOH_CTRL_NONE;
+ if (tu_desc_subtype(p_desc) != AUDIO20_CS_AC_INTERFACE_FEATURE_UNIT) {
+ return false;
}
+ control_offset = 5;
+ control_size = 4;
+ channels = (uint8_t)((tu_desc_len(p_desc) - 6u) / control_size - 1u);
+ mute_access = audioh_uac2_control_access(tu_le32toh(tu_unaligned_read32(&p_desc[control_offset])),
+ AUDIO20_FEATURE_UNIT_CTRL_MUTE_POS);
break;
#endif
default:
- break;
+ return false;
}
- return false;
+
+ uint8_t volume_master_access = AUDIOH_CTRL_NONE;
+ uint8_t volume_range_channel = TUSB_INDEX_INVALID_8;
+ bool volume_all_channels_writable = channels > 0;
+ for (uint8_t channel = 0; channel <= channels; channel++) {
+ uint8_t access;
+ #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2
+ if (p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2) {
+ const uint32_t controls = tu_le32toh(tu_unaligned_read32(&p_desc[control_offset + channel * control_size]));
+ access = audioh_uac2_control_access(controls, AUDIO20_FEATURE_UNIT_CTRL_VOLUME_POS);
+ } else
+ #endif
+ {
+ access = (p_desc[control_offset + channel * control_size] & AUDIO10_FU_CONTROL_BM_VOLUME) ? AUDIOH_CTRL_READ_WRITE
+ : AUDIOH_CTRL_NONE;
+ }
+ if (channel == 0) {
+ volume_master_access = access;
+ } else {
+ volume_all_channels_writable &= access == AUDIOH_CTRL_READ_WRITE;
+ }
+ if (access != AUDIOH_CTRL_NONE && volume_range_channel == TUSB_INDEX_INVALID_8) {
+ volume_range_channel = channel;
+ }
+ }
+
+ *info = (audioh_fu_info_t){.id = p_desc[3],
+ .source_id = p_desc[4],
+ .mute_access = mute_access,
+ .volume_master_access = volume_master_access,
+ .channels = channels,
+ .volume_range_channel = volume_range_channel,
+ .volume_all_channels_writable = volume_all_channels_writable};
+ return mute_access != AUDIOH_CTRL_NONE || volume_range_channel != TUSB_INDEX_INVALID_8;
}
#if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2
@@ -1103,7 +1154,8 @@ static void audioh_link_feature_units(audioh_interface_t *p_audio, const audioh_
}
const bool linked = (direction == TUSB_DIR_OUT) ? (fu.source_id == terminal.id) : (fu.id == terminal.source_id);
if (linked) {
- audioh_stream_set_feature_unit(stream, fu.id, fu.mute_access, fu.volume_access);
+ audioh_stream_set_feature_unit(stream, fu.id, fu.mute_access, fu.volume_master_access, fu.channels,
+ fu.volume_range_channel, fu.volume_all_channels_writable);
break;
}
}
@@ -1869,7 +1921,7 @@ bool tuh_audio_volume_range_get(uint8_t idx, uint8_t stream_idx, tuh_audio_volum
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_access != AUDIOH_CTRL_NONE, false);
+ TU_VERIFY(s != NULL && s->volume_range_channel != TUSB_INDEX_INVALID_8, false);
*range = s->volume_range;
return true;
}
@@ -2216,21 +2268,7 @@ uint32_t tuh_audio_read_available(uint8_t dev_idx, uint8_t stream_idx) {
// AUDIO CONTROL REQUESTS
//--------------------------------------------------------------------+
-// Release driver-owned request state before invoking the application callback,
-// allowing the callback to submit another Feature Unit request immediately.
-static void audioh_fu_set_complete(tuh_xfer_t *xfer) {
- const uint8_t idx = (uint8_t)xfer->user_data;
- audioh_ctrl_state_t *ctrl = &_audioh_itf[idx].ctrl;
- tuh_xfer_cb_t app_cb = ctrl->complete_cb;
- uintptr_t user_data = ctrl->user_data;
- ctrl->complete_cb = NULL;
- ctrl->fu_busy = false;
-
- xfer->user_data = user_data;
- if (app_cb != NULL) {
- app_cb(xfer);
- }
-}
+static void audioh_fu_set_complete(tuh_xfer_t *xfer);
enum {
AUDIOH_FU_VALUE_BOOL,
@@ -2303,6 +2341,38 @@ tusb_xfer_result_t tuh_audio_control_xfer_sync(uint8_t idx, uint8_t entity_id, t
return xfer.result;
}
+// Continue a master-volume request across logical channels. Driver-owned
+// request state is released before the final application callback so another
+// Feature Unit request can be submitted from that callback.
+static void audioh_fu_set_complete(tuh_xfer_t *xfer) {
+ const uint8_t idx = (uint8_t)xfer->user_data;
+ audioh_interface_t *p_audio = &_audioh_itf[idx];
+ audioh_ctrl_state_t *ctrl = &p_audio->ctrl;
+
+ if (xfer->result == XFER_RESULT_SUCCESS && ctrl->fu.control.channel < ctrl->fu.control.last_channel) {
+ tuh_audio_stream_t *s = (tuh_audio_stream_t *)ctrl->value;
+ ctrl->fu.control.channel++;
+ const uint8_t request_code = audioh_control_cur_request(p_audio->protocol, TUSB_DIR_OUT);
+ tuh_xfer_t next_xfer = {.complete_cb = audioh_fu_set_complete, .user_data = (uintptr_t)idx};
+ if (audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_OUT, request_code, AUDIO10_FU_CTRL_VOLUME,
+ ctrl->fu.control.channel, audioh_fu_ctrl(&_audioh_epbuf[idx]), 2, &next_xfer)) {
+ return;
+ }
+ xfer->result = XFER_RESULT_FAILED;
+ }
+
+ tuh_xfer_cb_t app_cb = ctrl->complete_cb;
+ uintptr_t user_data = ctrl->user_data;
+ ctrl->complete_cb = NULL;
+ ctrl->fu_busy = false;
+ ctrl->value = NULL;
+
+ xfer->user_data = user_data;
+ if (app_cb != NULL) {
+ app_cb(xfer);
+ }
+}
+
static void audioh_fu_value_store(audioh_ctrl_state_t *ctrl, audioh_epbuf_t *epbuf) {
if (ctrl->fu.control.value_type == AUDIOH_FU_VALUE_BOOL) {
*((bool *)ctrl->value) = audioh_fu_ctrl(epbuf)[0] != 0;
@@ -2387,7 +2457,7 @@ static bool audioh_mount_feature_unit_submit(uint8_t idx) {
const tusb_control_request_t request = {
.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN},
.bRequest = uac2 ? AUDIO20_CS_REQ_RANGE : audioh_fu_volume_range_request(ctrl->fu.mount.range_step),
- .wValue = tu_htole16(tu_u16(selector, 0)),
+ .wValue = tu_htole16(tu_u16(selector, s->volume_range_channel)),
.wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)),
.wLength = tu_htole16(uac2 ? 8u : 2u),
};
@@ -2406,14 +2476,16 @@ static void audioh_mount_feature_unit_next(uint8_t idx) {
while (ctrl->fu.mount.stream_idx < p_audio->stream_count) {
tuh_audio_stream_t *s = audioh_get_stream_by_idx_unchecked(p_audio, ctrl->fu.mount.stream_idx);
- if (s->volume_access != AUDIOH_CTRL_NONE) {
+ if (s->volume_range_channel != TUSB_INDEX_INVALID_8) {
s->volume_range = (tuh_audio_volume_range_t){0};
ctrl->fu.mount.range_step = AUDIOH_VOLUME_RANGE_MIN;
ctrl->fu_busy = true;
if (audioh_mount_feature_unit_submit(idx)) {
return;
}
- s->volume_access = AUDIOH_CTRL_NONE;
+ s->volume_master_access = AUDIOH_CTRL_NONE;
+ s->volume_range_channel = TUSB_INDEX_INVALID_8;
+ s->volume_all_channels_writable = false;
if (s->mute_access == AUDIOH_CTRL_NONE) {
s->feature_unit_id = 0;
}
@@ -2464,8 +2536,10 @@ static void audioh_mount_feature_unit_complete(tuh_xfer_t *xfer) {
const uint32_t expected_len = uac2 ? 8u : 2u;
if (xfer->result != XFER_RESULT_SUCCESS || xfer->actual_len != expected_len) {
- s->volume_access = AUDIOH_CTRL_NONE;
- s->volume_range = (tuh_audio_volume_range_t){0};
+ s->volume_master_access = AUDIOH_CTRL_NONE;
+ s->volume_range_channel = TUSB_INDEX_INVALID_8;
+ s->volume_all_channels_writable = false;
+ s->volume_range = (tuh_audio_volume_range_t){0};
if (s->mute_access == AUDIOH_CTRL_NONE) {
s->feature_unit_id = 0;
}
@@ -2475,17 +2549,24 @@ static void audioh_mount_feature_unit_complete(tuh_xfer_t *xfer) {
audioh_mount_feature_unit_next(idx);
}
-static bool audioh_fu_set(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint16_t value, uint8_t width,
- tuh_xfer_cb_t complete_cb, uintptr_t user_data) {
+static bool audioh_fu_set(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel,
+ uint8_t last_channel, uint16_t value, uint8_t width, 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, false);
tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx);
TU_VERIFY(s && s->feature_unit_id != 0, false);
if (control_selector == AUDIO10_FU_CTRL_MUTE) {
- TU_VERIFY(s->mute_access == AUDIOH_CTRL_READ_WRITE, false);
+ TU_VERIFY(channel == 0 && last_channel == 0 && s->mute_access == AUDIOH_CTRL_READ_WRITE, false);
} else if (control_selector == AUDIO10_FU_CTRL_VOLUME) {
- TU_VERIFY(s->volume_access == AUDIOH_CTRL_READ_WRITE, false);
+ TU_VERIFY(s->volume_range_channel != TUSB_INDEX_INVALID_8 && channel <= last_channel, false);
+ if (channel == 0) {
+ TU_VERIFY(last_channel == 0 && s->volume_master_access == AUDIOH_CTRL_READ_WRITE, false);
+ } else {
+ TU_VERIFY(last_channel <= s->feature_unit_channels, false);
+ TU_VERIFY(channel == last_channel || s->volume_all_channels_writable, false);
+ }
}
const uint8_t request_code = audioh_control_cur_request(p_audio->protocol, TUSB_DIR_OUT);
@@ -2501,40 +2582,53 @@ static bool audioh_fu_set(uint8_t idx, uint8_t stream_idx, uint8_t control_selec
val_buf[1] = (uint8_t)((value >> 8) & 0xFF);
}
- tuh_xfer_t xfer = {.complete_cb = complete_cb, .user_data = user_data};
-
if (complete_cb == NULL) {
- const bool result = audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_OUT, request_code, control_selector, 0,
- val_buf, width, &xfer);
- ctrl->fu_busy = false;
+ bool result = true;
+ for (uint8_t current_channel = channel; current_channel <= last_channel; current_channel++) {
+ tuh_xfer_t xfer = {.complete_cb = NULL, .user_data = user_data};
+ result = audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_OUT, request_code, control_selector,
+ current_channel, val_buf, width, &xfer);
+ if (!result || xfer.result != XFER_RESULT_SUCCESS) {
+ break;
+ }
+ }
+ ctrl->fu_busy = false;
return result;
}
- ctrl->complete_cb = complete_cb;
- ctrl->user_data = user_data;
- xfer.complete_cb = audioh_fu_set_complete;
- xfer.user_data = (uintptr_t)idx;
+ ctrl->complete_cb = complete_cb;
+ ctrl->user_data = user_data;
+ ctrl->value = s;
+ ctrl->fu.control.channel = channel;
+ ctrl->fu.control.last_channel = last_channel;
+ tuh_xfer_t xfer = {.complete_cb = audioh_fu_set_complete, .user_data = (uintptr_t)idx};
- if (!audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_OUT, request_code, control_selector, 0, val_buf, width,
- &xfer)) {
+ if (!audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_OUT, request_code, control_selector, channel, val_buf,
+ width, &xfer)) {
ctrl->complete_cb = NULL;
+ ctrl->value = NULL;
ctrl->fu_busy = false;
return false;
}
return true;
}
-static bool audioh_fu_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, void *value, uint8_t width,
- uint8_t value_type, tuh_xfer_cb_t complete_cb, uintptr_t user_data) {
+static bool audioh_fu_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, void *value,
+ uint8_t width, 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);
tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx);
TU_VERIFY(s && s->feature_unit_id != 0, false);
if (control_selector == AUDIO10_FU_CTRL_MUTE) {
- TU_VERIFY(s->mute_access != AUDIOH_CTRL_NONE, false);
+ TU_VERIFY(channel == 0 && s->mute_access != AUDIOH_CTRL_NONE, false);
} else if (control_selector == AUDIO10_FU_CTRL_VOLUME) {
- TU_VERIFY(s->volume_access != AUDIOH_CTRL_NONE, false);
+ TU_VERIFY(s->volume_range_channel != TUSB_INDEX_INVALID_8, false);
+ if (channel == 0) {
+ TU_VERIFY(s->volume_master_access != AUDIOH_CTRL_NONE, false);
+ } else {
+ TU_VERIFY(channel <= s->feature_unit_channels, false);
+ }
}
const uint8_t request_code = audioh_control_cur_request(p_audio->protocol, TUSB_DIR_IN);
@@ -2550,7 +2644,7 @@ static bool audioh_fu_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selec
// The synchronous transfer completes before its driver-owned response is
// converted to host order.
tuh_xfer_t xfer = {.complete_cb = NULL, .user_data = user_data};
- if (!audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_IN, request_code, control_selector, 0,
+ if (!audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_IN, request_code, control_selector, channel,
audioh_fu_ctrl(epbuf), width, &xfer)) {
ctrl->fu_busy = false;
return false;
@@ -2569,7 +2663,7 @@ static bool audioh_fu_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selec
ctrl->user_data = user_data;
tuh_xfer_t xfer = {.complete_cb = audioh_fu_get_complete, .user_data = (uintptr_t)idx};
- if (!audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_IN, request_code, control_selector, 0,
+ if (!audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_IN, request_code, control_selector, channel,
audioh_fu_ctrl(epbuf), width, &xfer)) {
ctrl->complete_cb = NULL;
ctrl->fu_busy = false;
@@ -2579,33 +2673,52 @@ static bool audioh_fu_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selec
}
bool tuh_audio_mute_set(uint8_t idx, uint8_t stream_idx, bool mute, tuh_xfer_cb_t complete_cb, uintptr_t user_data) {
- return audioh_fu_set(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, mute ? 1 : 0, 1, complete_cb, user_data);
+ return audioh_fu_set(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, 0, 0, mute ? 1 : 0, 1, 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) {
- return audioh_fu_get(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, mute, 1, AUDIOH_FU_VALUE_BOOL, complete_cb, user_data);
+ return audioh_fu_get(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, 0, mute, 1, 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) {
+static bool audioh_volume_normalize(uint8_t idx, uint8_t stream_idx, int16_t *volume) {
tuh_audio_volume_range_t range;
TU_VERIFY(tuh_audio_volume_range_get(idx, stream_idx, &range), false);
- if (volume != TUH_AUDIO_VOLUME_SILENCE) {
- TU_VERIFY(volume >= range.min && volume <= range.max && range.res != 0, false);
- const uint32_t offset = (uint32_t)((int32_t)volume - range.min);
+ if (*volume != TUH_AUDIO_VOLUME_SILENCE) {
+ TU_VERIFY(*volume >= range.min && *volume <= range.max && range.res != 0, false);
+ const uint32_t offset = (uint32_t)((int32_t)*volume - range.min);
const uint32_t steps = (offset + range.res / 2u) / range.res;
int32_t rounded = (int32_t)range.min + (int32_t)(steps * range.res);
if (rounded > range.max) {
rounded -= range.res;
}
- volume = (int16_t)rounded;
+ *volume = (int16_t)rounded;
+ }
+ return true;
+}
+
+bool tuh_audio_volume_set(uint8_t idx, uint8_t stream_idx, uint8_t channel, int16_t volume, tuh_xfer_cb_t complete_cb,
+ uintptr_t user_data) {
+ TU_VERIFY(audioh_volume_normalize(idx, stream_idx, &volume), false);
+ if (channel > 0) {
+ return audioh_fu_set(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, channel, channel, (uint16_t)volume, 2, complete_cb,
+ user_data);
+ }
+
+ audioh_interface_t *p_audio = &_audioh_itf[idx];
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx);
+ TU_VERIFY(s != NULL, false);
+ if (s->volume_master_access == AUDIOH_CTRL_READ_WRITE) {
+ return audioh_fu_set(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, 0, (uint16_t)volume, 2, complete_cb, user_data);
}
- return audioh_fu_set(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, (uint16_t)volume, 2, complete_cb, user_data);
+ TU_VERIFY(s->feature_unit_channels > 0 && s->volume_all_channels_writable, false);
+ return audioh_fu_set(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 1, s->feature_unit_channels, (uint16_t)volume, 2,
+ 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,
+bool tuh_audio_volume_get(uint8_t idx, uint8_t stream_idx, uint8_t channel, int16_t *volume, tuh_xfer_cb_t complete_cb,
uintptr_t user_data) {
- return audioh_fu_get(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, volume, 2, AUDIOH_FU_VALUE_I16, complete_cb, user_data);
+ return audioh_fu_get(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, channel, volume, 2, 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 3cb1bb00c..9ce8410c6 100644
--- a/src/class/audio/audio_host.h
+++ b/src/class/audio/audio_host.h
@@ -240,7 +240,10 @@ bool tuh_audio_mounted(uint8_t idx);
uint8_t tuh_audio_get_dev_addr(uint8_t 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.
+// Get the cached volume range. The driver reads the master channel when it
+// supports volume, otherwise the first logical channel with volume control.
+// This typed API assumes logical channels use the same range; applications
+// needing per-channel ranges can use tuh_audio_control_xfer().
bool tuh_audio_volume_range_get(uint8_t idx, uint8_t stream_idx, tuh_audio_volume_range_t *range);
//--------------------------------------------------------------------+
@@ -256,19 +259,29 @@ bool tuh_audio_control_xfer(uint8_t idx, uint8_t entity_id, tusb_dir_t direction
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. Volume SET accepts
-// TUH_AUDIO_VOLUME_SILENCE or a value within the cached range; finite values
-// are rounded to the nearest resolution step measured from the range minimum.
+// before tuh_audio_mount_cb() is invoked. Volume channel 0 selects the master;
+// a SET falls back to writing every logical channel when the master is not
+// writable and all logical channels advertise write access. The completion
+// callback is invoked once after the entire operation. A nonzero volume
+// channel directly selects that 1-based Feature Unit logical channel.
+// Per-channel capability is not cached; an unsupported channel is reported by
+// the control transfer.
+//
+// Volume SET accepts TUH_AUDIO_VOLUME_SILENCE or a value within the cached
+// range; finite values are rounded to the nearest resolution step measured
+// from the range minimum.
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,
+bool tuh_audio_volume_set(uint8_t idx, uint8_t stream_idx, uint8_t channel, 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,
+bool tuh_audio_volume_get(uint8_t idx, uint8_t stream_idx, uint8_t channel, int16_t *volume, tuh_xfer_cb_t complete_cb,
uintptr_t user_data);
//--------------------------------------------------------------------+
// Synchronous control requests block until the transfer completes and return
// its result. actual_len may be NULL when the received length is not needed.
+// Only use when audio streaming is stopped, otherwise the stream's isochronous
+// transfers may be disrupted and creating audible artifacts !
//--------------------------------------------------------------------+
tusb_xfer_result_t tuh_audio_control_xfer_sync(uint8_t idx, uint8_t entity_id, tusb_dir_t direction, uint8_t request,
uint8_t control_selector, uint8_t channel, void *buffer, uint16_t length,
@@ -285,13 +298,13 @@ TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_mute_get_sync(u
}
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);
+ uint8_t channel, int16_t volume) {
+ TU_API_SYNC(tuh_audio_volume_set, idx, stream_idx, channel, 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);
+ uint8_t channel, int16_t *volume) {
+ TU_API_SYNC(tuh_audio_volume_get, idx, stream_idx, channel, volume);
}
//--------------------------------------------------------------------+
diff --git a/test/unit-test/test/host/audio/test_audio_host.c b/test/unit-test/test/host/audio/test_audio_host.c
index 0f865a5bb..101a0b58b 100644
--- a/test/unit-test/test/host/audio/test_audio_host.c
+++ b/test/unit-test/test/host/audio/test_audio_host.c
@@ -259,6 +259,10 @@ uint32_t tu_edpt_stream_read_xfer(tu_edpt_stream_t *s) {
#define TEST_UAC1_FEATURE_UNIT(_id, _source_id) \
TEST_UAC1_FEATURE_UNIT_CTRL(_id, _source_id, AUDIO10_FU_CONTROL_BM_MUTE | AUDIO10_FU_CONTROL_BM_VOLUME)
+#define TEST_UAC1_FEATURE_UNIT_STEREO(_id, _source_id, _master, _channel_1, _channel_2) \
+ 10, TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT, _id, _source_id, 1, _master, _channel_1, \
+ _channel_2, 0
+
#define TEST_UAC1_OUTPUT_TERM(_id, _type, _source_id) \
9, TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL, _id, U16_TO_U8S_LE(_type), 0, _source_id, 0
@@ -311,6 +315,10 @@ uint32_t tu_edpt_stream_read_xfer(tu_edpt_stream_t *s) {
18, TUSB_DESC_CS_INTERFACE, AUDIO20_CS_AC_INTERFACE_FEATURE_UNIT, _id, _source_id, U32_TO_U8S_LE(_master_controls), \
U32_TO_U8S_LE(0), U32_TO_U8S_LE(0), 0
+#define TEST_UAC2_FEATURE_UNIT_STEREO_CHANNELS(_id, _source_id, _master, _channel_1, _channel_2) \
+ 18, TUSB_DESC_CS_INTERFACE, AUDIO20_CS_AC_INTERFACE_FEATURE_UNIT, _id, _source_id, U32_TO_U8S_LE(_master), \
+ U32_TO_U8S_LE(_channel_1), U32_TO_U8S_LE(_channel_2), 0
+
#define TEST_UAC2_AS_INTERFACE_NUM(_itf, _alt, _ep_count) \
9, TUSB_DESC_INTERFACE, _itf, _alt, _ep_count, TUSB_CLASS_AUDIO, AUDIO_SUBCLASS_STREAMING, \
@@ -431,6 +439,23 @@ static const uint8_t uac2_playback_read_only_feature_unit[] = {
TEST_UAC2_CS_DATA_EP,
};
+static const uint8_t uac2_playback_read_only_master_volume[] = {
+ TEST_UAC2_AC_HEADER(64),
+ TEST_UAC2_CLOCK_SOURCE(PLAYBACK_CLOCK, AUDIO20_CTRL_RW << AUDIO20_CLOCK_SOURCE_CTRL_CLK_FRQ_POS),
+ TEST_UAC2_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, PLAYBACK_CLOCK, 2),
+ TEST_UAC2_FEATURE_UNIT_STEREO_CHANNELS(PLAYBACK_FU, PLAYBACK_INPUT_TERM,
+ AUDIO20_CTRL_R << AUDIO20_FEATURE_UNIT_CTRL_VOLUME_POS,
+ AUDIO20_CTRL_RW << AUDIO20_FEATURE_UNIT_CTRL_VOLUME_POS,
+ AUDIO20_CTRL_RW << AUDIO20_FEATURE_UNIT_CTRL_VOLUME_POS),
+ TEST_UAC2_OUTPUT_TERM(PLAYBACK_OUTPUT_TERM, AUDIO_TERM_TYPE_OUT_HEADPHONES, PLAYBACK_FU, PLAYBACK_CLOCK),
+ TEST_UAC2_AS_ALT0,
+ TEST_UAC2_AS_INTERFACE(1, 1),
+ TEST_UAC2_AS_GENERAL(PLAYBACK_INPUT_TERM, 2),
+ TEST_UAC2_FORMAT(2, 16),
+ TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ASYNCHRONOUS, 192, 1),
+ TEST_UAC2_CS_DATA_EP,
+};
+
static const uint8_t uac2_playback_read_only_clock[] = {
TEST_UAC2_AC_HEADER(46),
TEST_UAC2_CLOCK_SOURCE(PLAYBACK_CLOCK, AUDIO20_CTRL_R << AUDIO20_CLOCK_SOURCE_CTRL_CLK_FRQ_POS),
@@ -554,6 +579,20 @@ static const uint8_t playback_fu_without_mute_volume[] = {
TEST_UAC1_CS_DATA_EP,
};
+static const uint8_t playback_fu_channel_volume_only[] = {
+ TEST_UAC1_AC_HEADER,
+ TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2),
+ TEST_UAC1_FEATURE_UNIT_STEREO(PLAYBACK_FU, PLAYBACK_INPUT_TERM, 0, AUDIO10_FU_CONTROL_BM_VOLUME,
+ AUDIO10_FU_CONTROL_BM_VOLUME),
+ TEST_UAC1_OUTPUT_TERM(PLAYBACK_OUTPUT_TERM, AUDIO_TERM_TYPE_OUT_HEADPHONES, PLAYBACK_FU),
+ TEST_UAC1_AS_ALT0,
+ TEST_UAC1_AS_INTERFACE(1, 1),
+ TEST_UAC1_AS_GENERAL(PLAYBACK_INPUT_TERM),
+ TEST_UAC1_FORMAT(2, 2, 16, 48000),
+ TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ADAPTIVE, 192, 1),
+ TEST_UAC1_CS_DATA_EP,
+};
+
static const uint8_t capture_fu_before_usb_output[] = {
TEST_UAC1_AC_HEADER,
TEST_UAC1_INPUT_TERM(CAPTURE_INPUT_TERM, AUDIO_TERM_TYPE_IN_GENERIC_MIC, 1),
@@ -761,10 +800,12 @@ static const uint8_t playback_11025_interval4[] = {
static uint8_t fu_cb_count;
static uintptr_t fu_cb_user_data;
+static tusb_xfer_result_t fu_cb_result;
static void feature_unit_complete(tuh_xfer_t *xfer) {
fu_cb_count++;
fu_cb_user_data = xfer->user_data;
+ fu_cb_result = xfer->result;
}
static void complete_interface_set(tusb_xfer_result_t result) {
@@ -910,6 +951,7 @@ void setUp(void) {
fu_cb_count = 0;
fu_cb_user_data = 0;
+ fu_cb_result = XFER_RESULT_INVALID;
TEST_ASSERT_TRUE(audioh_init());
}
@@ -1026,7 +1068,7 @@ void test_audio_host_mounts_uac2_and_sets_clock_before_activating_stream(void) {
TEST_ASSERT_EQUAL_UINT16(256, range.res);
control_xfer_count = 0;
- TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, (int16_t)(-6 * 256), feature_unit_complete, 42));
+ TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, (int16_t)(-6 * 256), feature_unit_complete, 42));
TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count);
TEST_ASSERT_EQUAL_UINT8(AUDIO20_CS_REQ_CUR, control_request.bRequest);
TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO20_FU_CTRL_VOLUME, 0), tu_le16toh(control_request.wValue));
@@ -1113,10 +1155,10 @@ void test_audio_host_uac2_read_only_feature_controls_reject_set_and_allow_get(vo
TEST_ASSERT_TRUE(tuh_audio_mounted(0));
TEST_ASSERT_FALSE(tuh_audio_mute_set(0, 0, false, feature_unit_complete, 0));
- TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, 0, feature_unit_complete, 0));
+ TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, 0, 0, feature_unit_complete, 0));
int16_t volume = 0;
- TEST_ASSERT_TRUE(tuh_audio_volume_get(0, 0, &volume, feature_unit_complete, 7));
+ TEST_ASSERT_TRUE(tuh_audio_volume_get(0, 0, 0, &volume, feature_unit_complete, 7));
TEST_ASSERT_EQUAL_UINT8(AUDIO20_CS_REQ_CUR, control_request.bRequest);
TEST_ASSERT_EQUAL(TUSB_DIR_IN, control_request.bmRequestType_bit.direction);
complete_control_xfer_with_u16((uint16_t)(-12 * 256));
@@ -1124,6 +1166,30 @@ void test_audio_host_uac2_read_only_feature_controls_reject_set_and_allow_get(vo
TEST_ASSERT_EQUAL_UINT32(7, fu_cb_user_data);
}
+void test_audio_host_uac2_read_only_master_volume_set_fans_out_to_writable_channels(void) {
+ open_descriptors(uac2_playback_read_only_master_volume, sizeof(uac2_playback_read_only_master_volume));
+ TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF));
+ complete_uac2_clock_range(44100, 48000);
+
+ TEST_ASSERT_EQUAL_UINT8(AUDIO20_CS_REQ_RANGE, control_request.bRequest);
+ TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO20_FU_CTRL_VOLUME, 0), tu_le16toh(control_request.wValue));
+ complete_uac2_volume_range((int16_t)(-90 * 256), (int16_t)(6 * 256), 256);
+ TEST_ASSERT_TRUE(tuh_audio_mounted(0));
+
+ control_xfer_count = 0;
+ TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, -6 * 256, feature_unit_complete, 0x1234));
+ TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count);
+ TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO20_FU_CTRL_VOLUME, 1), tu_le16toh(control_request.wValue));
+ complete_control_xfer(XFER_RESULT_SUCCESS);
+
+ TEST_ASSERT_EQUAL_UINT8(2, control_xfer_count);
+ TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO20_FU_CTRL_VOLUME, 2), tu_le16toh(control_request.wValue));
+ TEST_ASSERT_EQUAL_UINT8(0, fu_cb_count);
+ complete_control_xfer(XFER_RESULT_SUCCESS);
+ TEST_ASSERT_EQUAL_UINT8(1, fu_cb_count);
+ TEST_ASSERT_EQUAL_HEX32(0x1234, fu_cb_user_data);
+}
+
void test_audio_host_uac2_shared_clock_is_discovered_once_for_both_streams(void) {
open_descriptors(uac2_duplex_shared_clock, sizeof(uac2_duplex_shared_clock));
TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF));
@@ -1664,7 +1730,7 @@ void test_audio_host_rejects_invalid_cached_volume_range(void) {
TEST_ASSERT_FALSE(tuh_audio_volume_range_get(0, 0, &range));
}
-void test_audio_host_ignores_feature_unit_without_master_mute_or_volume(void) {
+void test_audio_host_ignores_feature_unit_without_mute_or_volume(void) {
tuh_audio_volume_range_t range;
mount_descriptors(playback_fu_without_mute_volume, sizeof(playback_fu_without_mute_volume));
@@ -1679,14 +1745,14 @@ void test_audio_host_typed_mute_and_volume_controls(void) {
mount_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal));
TEST_ASSERT_TRUE(tuh_audio_volume_range_get(0, 0, &range));
- TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, (int16_t)(range.max + 1), NULL, 0));
+ TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, 0, (int16_t)(range.max + 1), NULL, 0));
TEST_ASSERT_TRUE(tuh_audio_mute_set(0, 0, true, feature_unit_complete, 0x1234));
TEST_ASSERT_EQUAL_HEX8(AUDIO10_CS_REQ_SET_CUR, control_request.bRequest);
TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_MUTE, 0), tu_le16toh(control_request.wValue));
TEST_ASSERT_EQUAL_UINT16(1, tu_le16toh(control_request.wLength));
TEST_ASSERT_EQUAL_HEX8(1, control_buffer[0]);
- TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, -6 * 256, feature_unit_complete, 0));
+ TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, 0, -6 * 256, feature_unit_complete, 0));
complete_control_xfer(XFER_RESULT_SUCCESS);
TEST_ASSERT_TRUE(tuh_audio_mute_get(0, 0, &mute, feature_unit_complete, 0x2345));
@@ -1696,30 +1762,85 @@ void test_audio_host_typed_mute_and_volume_controls(void) {
complete_control_xfer(XFER_RESULT_SUCCESS);
TEST_ASSERT_TRUE(mute);
- TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, -6 * 256, feature_unit_complete, 0x3456));
+ TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, -6 * 256, feature_unit_complete, 0x3456));
TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 0), tu_le16toh(control_request.wValue));
TEST_ASSERT_EQUAL_UINT16(2, tu_le16toh(control_request.wLength));
TEST_ASSERT_EQUAL_HEX8_ARRAY(((uint8_t[]){0x00, 0xFA}), control_buffer, 2);
complete_control_xfer(XFER_RESULT_SUCCESS);
- TEST_ASSERT_TRUE(tuh_audio_volume_get(0, 0, &volume, feature_unit_complete, 0x4567));
+ TEST_ASSERT_TRUE(tuh_audio_volume_get(0, 0, 0, &volume, feature_unit_complete, 0x4567));
complete_control_xfer_with_u16((uint16_t)(-12 * 256));
TEST_ASSERT_EQUAL_INT16(-12 * 256, volume);
TEST_ASSERT_EQUAL_UINT8(4, fu_cb_count);
TEST_ASSERT_EQUAL_HEX32(0x4567, fu_cb_user_data);
}
+void test_audio_host_uac1_channel_volume_supports_individual_and_stream_controls(void) {
+ int16_t volume = 0;
+ open_descriptors(playback_fu_channel_volume_only, sizeof(playback_fu_channel_volume_only));
+ TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF));
+ TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 1), tu_le16toh(control_request.wValue));
+ complete_mount();
+
+ TEST_ASSERT_FALSE(tuh_audio_volume_get(0, 0, 0, &volume, feature_unit_complete, 0));
+ TEST_ASSERT_TRUE(tuh_audio_volume_get(0, 0, 2, &volume, feature_unit_complete, 0x2345));
+ TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 2), tu_le16toh(control_request.wValue));
+ complete_control_xfer_with_u16((uint16_t)(-12 * 256));
+ TEST_ASSERT_EQUAL_INT16(-12 * 256, volume);
+ TEST_ASSERT_EQUAL_UINT8(1, fu_cb_count);
+
+ TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 1, -3 * 256, feature_unit_complete, 0x3456));
+ TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 1), tu_le16toh(control_request.wValue));
+ complete_control_xfer(XFER_RESULT_SUCCESS);
+ TEST_ASSERT_EQUAL_UINT8(2, fu_cb_count);
+
+ const uint8_t before_stream_set = control_xfer_count;
+ TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, -6 * 256, feature_unit_complete, 0x4567));
+ TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 1), tu_le16toh(control_request.wValue));
+ TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, 2, 0, feature_unit_complete, 0));
+ complete_control_xfer(XFER_RESULT_SUCCESS);
+ TEST_ASSERT_EQUAL_UINT8(before_stream_set + 2, control_xfer_count);
+ TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 2), tu_le16toh(control_request.wValue));
+ TEST_ASSERT_EQUAL_UINT8(2, fu_cb_count);
+ complete_control_xfer(XFER_RESULT_SUCCESS);
+ TEST_ASSERT_EQUAL_UINT8(3, fu_cb_count);
+ TEST_ASSERT_EQUAL_HEX32(0x4567, fu_cb_user_data);
+}
+
+void test_audio_host_channel_volume_fanout_stops_on_transfer_failure(void) {
+ mount_descriptors(playback_fu_channel_volume_only, sizeof(playback_fu_channel_volume_only));
+
+ TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, -6 * 256, feature_unit_complete, 0x5678));
+ TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count);
+ complete_control_xfer(XFER_RESULT_STALLED);
+
+ TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count);
+ TEST_ASSERT_EQUAL_UINT8(1, fu_cb_count);
+ TEST_ASSERT_EQUAL_HEX32(0x5678, fu_cb_user_data);
+ TEST_ASSERT_EQUAL(XFER_RESULT_STALLED, fu_cb_result);
+}
+
+void test_audio_host_channel_volume_fanout_supports_blocking_requests(void) {
+ tusb_xfer_result_t result = XFER_RESULT_INVALID;
+ mount_descriptors(playback_fu_channel_volume_only, sizeof(playback_fu_channel_volume_only));
+
+ TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, -6 * 256, NULL, (uintptr_t)&result));
+ TEST_ASSERT_EQUAL_UINT8(2, control_xfer_count);
+ TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 2), tu_le16toh(control_request.wValue));
+ TEST_ASSERT_EQUAL_UINT8(0, fu_cb_count);
+}
+
void test_audio_host_volume_set_accepts_silence_and_rounds_unaligned_values(void) {
tuh_audio_volume_range_t range;
mount_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal));
TEST_ASSERT_TRUE(tuh_audio_volume_range_get(0, 0, &range));
- TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, (int16_t)(-6 * 256 + 100), feature_unit_complete, 0));
+ TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, (int16_t)(-6 * 256 + 100), feature_unit_complete, 0));
TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count);
TEST_ASSERT_EQUAL_HEX8_ARRAY(((uint8_t[]){0x00, 0xFA}), control_buffer, 2);
complete_control_xfer(XFER_RESULT_SUCCESS);
- TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, TUH_AUDIO_VOLUME_SILENCE, feature_unit_complete, 0));
+ TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, TUH_AUDIO_VOLUME_SILENCE, feature_unit_complete, 0));
TEST_ASSERT_EQUAL_UINT8(2, control_xfer_count);
TEST_ASSERT_EQUAL_HEX8_ARRAY(((uint8_t[]){0x00, 0x80}), control_buffer, 2);
complete_control_xfer(XFER_RESULT_SUCCESS);