summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/host/audio_host/README.md20
-rw-r--r--examples/host/audio_host/src/audio_app.c73
-rw-r--r--src/class/audio/audio_host.c315
-rw-r--r--src/class/audio/audio_host.h46
-rw-r--r--test/unit-test/test/host/audio/test_audio_host.c166
5 files changed, 539 insertions, 81 deletions
diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md
index bbb3a7684..5413895a4 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 devices
- Discovers the device's logical streams (capture/playback) and their supported configurations (discrete tuples only)
-- Reports the Feature Unit ID associated with each stream and sets the master volume when one is available
+- Reports each stream's master mute/volume capabilities and cached volume range
- Configures and starts an S16_LE capture stream (48 kHz preferred, 44.1 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: `tuh_audio_read()` / `tuh_audio_write()` queue frames; the driver schedules transfers at the endpoint's polling interval
@@ -26,7 +26,7 @@ The echo needs a matching S16_LE playback stream at the capture sample rate; dev
- Explicit feedback endpoint data is ignored. Asynchronous playback still uses the nominal sample rate, but device/host clock drift is not corrected and may cause underruns, overruns, or audible pops and clicks. An implicit-feedback IN endpoint is treated as an ordinary audio-data endpoint and is not used to pace playback.
- UAC1 Type I Format descriptors with `bSamFreqType == 0` are unsupported; the driver requires a list of discrete sampling frequencies.
-- Feature Unit mute, bass, mid, treble, volume, delay, AGC, bass boost, and loudness controls are supported. Graphic EQ and unknown control widths are rejected, and only one Feature Unit request may be in flight per device.
+- Master mute and volume controls are discovered before the mount callback, including the volume MIN/MAX/RES range. Feature Units without master mute or volume are ignored. The typed API controls the master channel; the lower-level Feature Unit API remains available for fixed-width UAC1 controls on the associated unit.
- The UAC1 `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
@@ -65,10 +65,10 @@ make BOARD=<your_board> flash
2. Connect a USB Audio device (UAC 1.0) to the USB host port
3. Open a serial terminal to view output
4. The example will:
- - Print each stream's Feature Unit ID and supported configurations when mounted
+ - Print each stream's Feature Unit ID, master mute/volume capabilities, cached volume range, and supported configurations when mounted
- Look for an S16_LE capture configuration at a preferred sample rate (48 kHz first, 44.1 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)
- - Set the microphone and speaker master volume to `0x0600` when their streams have a Feature Unit
+ - Read/unmute the microphone and speaker Feature Units and set supported master volumes near -6 dB
- Drain the capture FIFO in `audio_app_task_read()` and queue the frames into the playback FIFO; 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()`; a failed stream is restarted automatically 100 ms after the error callback
@@ -79,17 +79,25 @@ TinyUSB Host USB Audio Example
Connect a USB Audio Device (UAC 1.0) to test
Audio device mounted: idx=0 addr=1
capture stream 1 Feature Unit ID: 5, configurations: 2
+ master mute supported
+ master 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 Feature Unit ID: 2, configurations: 2
+ master mute supported
+ master 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 48 kHz S16_LE capture (2 channels)
Microphone configured
- Microphone Feature Unit 5 master volume set: 0x0600
+ Microphone Feature Unit 5 master mute: off
+ Microphone Feature Unit 5 master volume: 0 (1/256 dB)
+ Microphone master volume set: -1536 (1/256 dB)
Configuring 48 kHz S16_LE playback (2 channels)
Speaker configured
- Speaker Feature Unit 2 master volume set: 0x0600
+ Speaker Feature Unit 2 master mute: off
+ Speaker Feature Unit 2 master volume: 0 (1/256 dB)
+ Speaker master 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 3102c28c9..9a1434284 100644
--- a/examples/host/audio_host/src/audio_app.c
+++ b/examples/host/audio_host/src/audio_app.c
@@ -28,11 +28,10 @@
// - AUDIO_MAX_FRAME_COUNT: buffer holds up to 48 frames (1 ms of 48 kHz)
// - AUDIO_MAX_CHANNELS: maximum channels of the capture/playback stream
// - SAMPLE_RATES: sample rates tried in order, first match wins (44.1 kHz stereo by default)
-#define AUDIO_MAX_FRAME_COUNT 48
-#define AUDIO_MAX_CHANNELS 2
-#define SAMPLE_RATES {48000, 44100}
-// UAC1 volume values are signed 1/256 dB; 0x0600 selects +6 dB.
-#define FEATURE_UNIT_VOLUME 0x0600
+#define AUDIO_MAX_FRAME_COUNT 48
+#define AUDIO_MAX_CHANNELS 2
+#define SAMPLE_RATES {48000, 44100}
+#define FEATURE_UNIT_VOLUME_DB (-6 * 256)
static uint8_t audio_idx = TUSB_INDEX_INVALID_8; // index of the selected audio device
static uint8_t cap_stream_idx = TUSB_INDEX_INVALID_8; // capture stream index
static uint8_t spk_stream_idx = TUSB_INDEX_INVALID_8; // playback stream index
@@ -255,17 +254,6 @@ void led_blinking_task(void) {
err_cb_count = 0;
#endif
-#if 0
- // Print the current Feature Unit volume, which is set to 0x0600 in mic_configured() and can be changed by the device.
- uint16_t volume = 0x0001;
- tuh_audio_feature_unit_get_sync(audio_idx, cap_stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume);
- printf(" Feature Unit volume get: 0x%04x\r\n", (unsigned int)volume);
- uint16_t mute = 0x0000;
- tuh_audio_feature_unit_get_sync(audio_idx, cap_stream_idx, AUDIO10_FU_CTRL_MUTE, 0, &mute);
- mute=!mute; // toggle mute for demonstration
- tuh_audio_feature_unit_set_sync(audio_idx, cap_stream_idx, AUDIO10_FU_CTRL_MUTE, 0, mute);
- printf(" Feature Unit mute set: 0x%04x\r\n", (unsigned int)mute);
-#endif
}
//--------------------------------------------------------------------+
@@ -377,6 +365,14 @@ static void print_stream_configs(uint8_t idx, uint8_t stream_idx) {
const uint8_t feature_unit_id = tuh_audio_get_feature_unit_id(idx, stream_idx);
printf(" %s stream %u Feature Unit ID: %u, configurations: %u\r\n", dir_name, stream_idx, feature_unit_id,
tuh_audio_config_count(idx, stream_idx));
+ tuh_audio_volume_range_t range;
+ if (tuh_audio_mute_supported(idx, 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,
+ (unsigned)range.res);
+ }
for (uint8_t i = 0; i < tuh_audio_config_count(idx, stream_idx); i++) {
tuh_audio_stream_config_t config;
if (tuh_audio_config_get(idx, stream_idx, i, &config)) {
@@ -386,19 +382,44 @@ static void print_stream_configs(uint8_t idx, uint8_t stream_idx) {
}
}
-static void set_stream_volume(uint8_t idx, uint8_t stream_idx, const char *stream_name) {
+static void configure_stream_controls(uint8_t idx, uint8_t stream_idx, const char *stream_name) {
const uint8_t feature_unit_id = tuh_audio_get_feature_unit_id(idx, stream_idx);
if (feature_unit_id == 0) {
- printf(" %s stream has no Feature Unit\r\n", stream_name);
+ printf(" %s stream has no master mute/volume Feature Unit\r\n", stream_name);
return;
}
- uint16_t volume = FEATURE_UNIT_VOLUME;
- tusb_xfer_result_t result = tuh_audio_feature_unit_set_sync(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, volume);
- if (result == XFER_RESULT_SUCCESS) {
- printf(" %s Feature Unit %u master volume set: 0x%04x\r\n", stream_name, feature_unit_id, (unsigned int)volume);
- } else {
- printf(" Setting %s Feature Unit %u volume failed: result=%u\r\n", stream_name, feature_unit_id, result);
+ if (tuh_audio_mute_supported(idx, stream_idx)) {
+ bool mute;
+ tusb_xfer_result_t result = tuh_audio_mute_get_sync(idx, stream_idx, &mute);
+ if (result == XFER_RESULT_SUCCESS) {
+ printf(" %s Feature Unit %u master mute: %s\r\n", stream_name, feature_unit_id, mute ? "on" : "off");
+ result = tuh_audio_mute_set_sync(idx, stream_idx, false);
+ }
+ if (result != XFER_RESULT_SUCCESS) {
+ printf(" Accessing %s master mute failed: result=%u\r\n", stream_name, result);
+ }
+ }
+
+ tuh_audio_volume_range_t range;
+ if (tuh_audio_volume_range_get(idx, stream_idx, &range)) {
+ int16_t volume;
+ tusb_xfer_result_t result = tuh_audio_volume_get_sync(idx, stream_idx, &volume);
+ if (result == XFER_RESULT_SUCCESS) {
+ printf(" %s Feature Unit %u master volume: %d (1/256 dB)\r\n", stream_name, feature_unit_id, 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);
+ }
+ }
+ if (result != XFER_RESULT_SUCCESS) {
+ printf(" Accessing %s master volume failed: result=%u\r\n", stream_name, result);
+ }
}
}
@@ -462,7 +483,7 @@ static void tuh_audio_mount_async(uintptr_t param) {
// one ms of audio at the selected rate, rounded down to whole frames
audio_frame_count = sample_rate / 1000;
printf(" Microphone configured\r\n");
- set_stream_volume(idx, stream_idx, "Microphone");
+ configure_stream_controls(idx, stream_idx, "Microphone");
mic_ready = tuh_audio_start(idx, stream_idx);
capture_found = true;
break;
@@ -519,7 +540,7 @@ static void tuh_audio_mount_async(uintptr_t param) {
// playback-only device: set the frame cadence from the selected rate
audio_frame_count = spk_config.sample_rate / 1000;
sine_phase = 0;
- set_stream_volume(idx, spk_stream_idx, "Speaker");
+ configure_stream_controls(idx, spk_stream_idx, "Speaker");
if (capture_found) {
// Start in the mic-only phase without briefly activating playback first.
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)
//--------------------------------------------------------------------+
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 2b0f2fa3d..f02c44095 100644
--- a/test/unit-test/test/host/audio/test_audio_host.c
+++ b/test/unit-test/test/host/audio/test_audio_host.c
@@ -199,9 +199,11 @@ uint32_t tu_edpt_stream_read_xfer(tu_edpt_stream_t *s) {
12, TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AC_INTERFACE_INPUT_TERMINAL, _id, U16_TO_U8S_LE(_type), 0, _channels, \
U16_TO_U8S_LE(AUDIO10_CHANNEL_CONFIG_NON_PREDEFINED), 0, 0
-#define TEST_UAC1_FEATURE_UNIT(_id, _source_id) \
- 9, TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT, _id, _source_id, 2, \
- U16_TO_U8S_LE(AUDIO10_FU_CONTROL_BM_VOLUME), 0
+#define TEST_UAC1_FEATURE_UNIT_CTRL(_id, _source_id, _controls) \
+ 9, TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT, _id, _source_id, 2, U16_TO_U8S_LE(_controls), 0
+
+#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_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 +313,19 @@ static const uint8_t playback_fu_before_terminal[] = {
TEST_UAC1_CS_DATA_EP,
};
+static const uint8_t playback_fu_without_mute_volume[] = {
+ TEST_UAC1_AC_HEADER,
+ TEST_UAC1_FEATURE_UNIT_CTRL(PLAYBACK_FU, PLAYBACK_INPUT_TERM, AUDIO10_FU_CONTROL_BM_BASS),
+ TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2),
+ 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),
@@ -504,6 +519,13 @@ static void complete_control_xfer(tusb_xfer_result_t result) {
xfer.complete_cb(&xfer);
}
+static void complete_control_xfer_with_u16(uint16_t value) {
+ TEST_ASSERT_NOT_NULL(control_xfer.buffer);
+ tu_unaligned_write16(control_xfer.buffer, tu_htole16(value));
+ control_xfer.actual_len = 2;
+ complete_control_xfer(XFER_RESULT_SUCCESS);
+}
+
void setUp(void) {
interface_set_result = true;
memset(&interface_xfer, 0, sizeof(interface_xfer));
@@ -553,7 +575,24 @@ static void open_descriptors(const uint8_t *desc, uint16_t desc_len) {
static void mount_descriptors(const uint8_t *desc, uint16_t desc_len) {
open_descriptors(desc, desc_len);
TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF));
+ while (!tuh_audio_mounted(0)) {
+ switch (control_request.bRequest) {
+ case AUDIO10_CS_REQ_GET_MIN:
+ complete_control_xfer_with_u16((uint16_t)(-90 * 256));
+ break;
+ case AUDIO10_CS_REQ_GET_MAX:
+ complete_control_xfer_with_u16(6 * 256);
+ break;
+ case AUDIO10_CS_REQ_GET_RES:
+ complete_control_xfer_with_u16(256);
+ break;
+ default:
+ TEST_FAIL_MESSAGE("Unexpected Feature Unit mount request");
+ break;
+ }
+ }
TEST_ASSERT_TRUE(tuh_audio_mounted(0));
+ control_xfer_count = 0;
}
void test_audio_host_ignores_explicit_feedback_endpoint(void) {
@@ -638,6 +677,22 @@ void test_audio_host_maps_duplex_fus_declared_before_usb_terminals(void) {
TEST_ASSERT_EQUAL_UINT8(CAPTURE_FU, tuh_audio_get_feature_unit_id(0, 1));
}
+void test_audio_host_reads_volume_ranges_for_both_streams_before_mount(void) {
+ const uint8_t feature_units[] = {PLAYBACK_FU, CAPTURE_FU};
+ open_descriptors(duplex_fus_before_usb_terminals, sizeof(duplex_fus_before_usb_terminals));
+
+ TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF));
+ for (uint8_t i = 0; i < TU_ARRAY_SIZE(feature_units); i++) {
+ TEST_ASSERT_EQUAL_HEX8(AUDIO10_CS_REQ_GET_MIN, control_request.bRequest);
+ TEST_ASSERT_EQUAL_HEX16(tu_u16(feature_units[i], AUDIO_AC_ITF), tu_le16toh(control_request.wIndex));
+ complete_control_xfer_with_u16((uint16_t)(-90 * 256));
+ complete_control_xfer_with_u16(6 * 256);
+ complete_control_xfer_with_u16(256);
+ TEST_ASSERT_EQUAL(i == TU_ARRAY_SIZE(feature_units) - 1, tuh_audio_mounted(0));
+ }
+ TEST_ASSERT_EQUAL_UINT8(6, control_xfer_count);
+}
+
void test_audio_host_sets_sampling_frequency_after_each_stream_activation(void) {
mount_descriptors(capture_fu_before_usb_output, sizeof(capture_fu_before_usb_output));
@@ -836,6 +891,111 @@ void test_audio_host_uses_correct_feature_unit_widths_and_serializes_requests(vo
TEST_ASSERT_EQUAL_HEX32(0x5678, fu_cb_user_data);
}
+void test_audio_host_reads_and_caches_feature_unit_controls_before_mount(void) {
+ tuh_audio_volume_range_t range;
+ open_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal));
+
+ TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF));
+ TEST_ASSERT_FALSE(tuh_audio_mounted(0));
+ TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count);
+ TEST_ASSERT_EQUAL_HEX8(AUDIO10_CS_REQ_GET_MIN, control_request.bRequest);
+ TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 0), tu_le16toh(control_request.wValue));
+ TEST_ASSERT_EQUAL_HEX16(tu_u16(PLAYBACK_FU, AUDIO_AC_ITF), tu_le16toh(control_request.wIndex));
+ TEST_ASSERT_EQUAL_UINT16(2, tu_le16toh(control_request.wLength));
+
+ complete_control_xfer_with_u16((uint16_t)(-90 * 256));
+ TEST_ASSERT_EQUAL_UINT8(2, control_xfer_count);
+ TEST_ASSERT_EQUAL_HEX8(AUDIO10_CS_REQ_GET_MAX, control_request.bRequest);
+ TEST_ASSERT_FALSE(tuh_audio_mounted(0));
+
+ complete_control_xfer_with_u16(6 * 256);
+ TEST_ASSERT_EQUAL_UINT8(3, control_xfer_count);
+ TEST_ASSERT_EQUAL_HEX8(AUDIO10_CS_REQ_GET_RES, control_request.bRequest);
+ TEST_ASSERT_FALSE(tuh_audio_mounted(0));
+
+ complete_control_xfer_with_u16(256);
+ TEST_ASSERT_TRUE(tuh_audio_mounted(0));
+ TEST_ASSERT_TRUE(tuh_audio_mute_supported(0, 0));
+ TEST_ASSERT_TRUE(tuh_audio_volume_range_get(0, 0, &range));
+ TEST_ASSERT_EQUAL_INT16(-90 * 256, range.min);
+ TEST_ASSERT_EQUAL_INT16(6 * 256, range.max);
+ TEST_ASSERT_EQUAL_UINT16(256, range.res);
+}
+
+void test_audio_host_mounts_with_mute_only_when_volume_range_fails(void) {
+ tuh_audio_volume_range_t range;
+ open_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal));
+
+ TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF));
+ complete_control_xfer_with_u16((uint16_t)(-40 * 256));
+ complete_control_xfer(XFER_RESULT_STALLED);
+
+ TEST_ASSERT_EQUAL_UINT8(2, control_xfer_count);
+ TEST_ASSERT_TRUE(tuh_audio_mounted(0));
+ TEST_ASSERT_TRUE(tuh_audio_mute_supported(0, 0));
+ TEST_ASSERT_FALSE(tuh_audio_volume_range_get(0, 0, &range));
+ TEST_ASSERT_EQUAL_UINT8(PLAYBACK_FU, tuh_audio_get_feature_unit_id(0, 0));
+}
+
+void test_audio_host_rejects_invalid_cached_volume_range(void) {
+ tuh_audio_volume_range_t range;
+ open_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal));
+
+ TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF));
+ complete_control_xfer_with_u16((uint16_t)(-20 * 256));
+ complete_control_xfer_with_u16(0);
+ complete_control_xfer_with_u16(0);
+
+ TEST_ASSERT_TRUE(tuh_audio_mounted(0));
+ TEST_ASSERT_TRUE(tuh_audio_mute_supported(0, 0));
+ TEST_ASSERT_FALSE(tuh_audio_volume_range_get(0, 0, &range));
+}
+
+void test_audio_host_ignores_feature_unit_without_master_mute_or_volume(void) {
+ tuh_audio_volume_range_t range;
+ mount_descriptors(playback_fu_without_mute_volume, sizeof(playback_fu_without_mute_volume));
+
+ TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_get_feature_unit_id(0, 0));
+ TEST_ASSERT_FALSE(tuh_audio_mute_supported(0, 0));
+ TEST_ASSERT_FALSE(tuh_audio_volume_range_get(0, 0, &range));
+}
+
+void test_audio_host_typed_mute_and_volume_controls(void) {
+ bool mute = false;
+ int16_t volume = 0;
+ 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_FALSE(tuh_audio_volume_set(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]);
+ complete_control_xfer(XFER_RESULT_SUCCESS);
+
+ TEST_ASSERT_TRUE(tuh_audio_mute_get(0, 0, &mute, feature_unit_complete, 0x2345));
+ TEST_ASSERT_EQUAL_HEX8(AUDIO10_CS_REQ_GET_CUR, control_request.bRequest);
+ control_xfer.buffer[0] = 1;
+ control_xfer.actual_len = 1;
+ 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_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));
+ 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_schedules_44100_hz_fractional_packets_with_max_packets_only(void) {
uint8_t samples[441 * 4] = {0};
mount_descriptors(playback_44100_max_packets_only, sizeof(playback_44100_max_packets_only));