diff options
Diffstat (limited to 'src/class')
| -rw-r--r-- | src/class/audio/audio_host.c | 2724 | ||||
| -rw-r--r-- | src/class/audio/audio_host.h | 361 |
2 files changed, 3085 insertions, 0 deletions
diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c new file mode 100644 index 000000000..d93ff535d --- /dev/null +++ b/src/class/audio/audio_host.c @@ -0,0 +1,2724 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Zhenjiang Zhang + * SPDX-FileCopyrightText: Copyright (c) 2026 HiFiPhile (Zixun LI) + * SPDX-License-Identifier: MIT + * + * This file is part of the TinyUSB stack. + */ + +// clang-format off +/* + * USB Audio Host driver architecture + * ================================== + * + * One audioh_interface_t represents an Audio Control (AC) interface and owns + * at most one logical stream in each direction. A capture stream receives + * isochronous IN data from the device; a playback stream sends isochronous OUT + * data to the device. Audio topology remains private, while applications see + * each stream as a flat list of format, sample-rate, and channel-count tuples. + * Internally, tuples using the same Audio Streaming (AS) alternate setting + * share one audioh_as_config_t and refer to a rate source by index. + * Only Type-I PCM configurations are exposed. UAC1 requires a discrete + * sampling-frequency list; UAC2 Clock Source ranges are expanded into the + * bounded public list. + * + * Mounting discovers the topology and completes any control requests needed + * to describe the public configurations: + * + * USB enumeration + * audioh_open() + * +-- validate and retain the AC descriptor range + * +-- audioh_parse_as() for each consecutive AS interface + * | +-- parse the protocol-specific AS and format descriptors + * | +-- associate the data and optional feedback endpoints + * | `-- store UAC1 rates or a UAC2 Clock Source reference + * +-- audioh_link_feature_units() + * `-- tuh_audio_descriptor_cb() + * + * audioh_set_config() + * +-- UAC2: audioh_mount_clock_next() + * | `-- RANGE/CUR completion -> next Clock Source + * | -> rebuild public configurations + * `-- audioh_mount_feature_unit_next() + * `-- volume RANGE completion -> next logical stream + * -> tuh_audio_mount_cb() + * -> usbh_driver_set_config_complete() + * + * 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 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: + * + * tuh_audio_configure(stream, configuration) + * +-- resolve the public tuple to AS and rate-source indices + * +-- close endpoints from the previous configuration + * +-- initialize frame size, FIFO, and playback scheduler state + * `-- audioh_stream_open_ep() (data and optional feedback EP) + * + * tuh_audio_start(stream) + * +-- UAC1: SET_INTERFACE(non-zero alt) + * | `-- optional endpoint SET_CUR(sample rate) + * +-- UAC2: optional Clock Source CUR(sample rate) + * | `-- SET_INTERFACE(non-zero alt) + * `-- audioh_stream_start_xfer() + * `-- tuh_audio_event_cb(START_COMPLETE) + * + * tuh_audio_stop(stream) + * +-- SET_INTERFACE(alt 0) and stop local transfer resubmission + * `-- completion -> tuh_audio_event_cb(STOP_COMPLETE) + * + * A successful start/stop API return means that the first control request was + * submitted. The corresponding event reports completion of the entire chain. + * UAC1 sets the rate after activating the endpoint because its control targets + * that endpoint; UAC2 sets the Clock Source before activating the AS interface. + * + * Once started, each endpoint completion prepares and submits its successor: + * + * host controller -> audioh_xfer_cb() + * +-- capture data + * | +-- copy whole audio frames to the overwrite FIFO + * | +-- tuh_audio_capture_cb() + * | `-- audioh_stream_capture_xfer() + * +-- playback data + * | +-- tuh_audio_playback_cb() + * | `-- audioh_stream_playback_xfer() + * | +-- calculate the next fractional packet size + * | +-- read a complete packet from the FIFO, or send silence + * | `-- submit the next OUT transfer + * `-- explicit feedback + * +-- validate and stage the Q10.14 or Q16.16 rate + * `-- audioh_stream_feedback_xfer() + * + * tuh_audio_read() and tuh_audio_write() access only the stream FIFOs and do + * not need to run from transfer callbacks. The FIFOs decouple application I/O + * from USB polling cadence and never expose partial interleaved audio frames. + * A transfer failure stops resubmission and is reported through + * tuh_audio_event_cb(XFER_FAILED). + */ +// clang-format on + +#include "tusb_option.h" + +#if (CFG_TUH_ENABLED && CFG_TUH_AUDIO) + + #include "host/usbh.h" + #include "host/usbh_pvt.h" + #include "audio_host.h" + + // Driver-specific log level; defaults to the host-stack log level. + #ifndef CFG_TUH_AUDIO_LOG_LEVEL + #define CFG_TUH_AUDIO_LOG_LEVEL CFG_TUH_LOG_LEVEL + #endif + + #define TU_LOG_DRV(...) TU_LOG(CFG_TUH_AUDIO_LOG_LEVEL, __VA_ARGS__) + + +//--------------------------------------------------------------------+ +// MACROS, CONSTANTS, AND TYPES +//--------------------------------------------------------------------+ + +enum { + STREAM_STATE_IDLE = 0, // No active configuration. + STREAM_STATE_READY // Configured and ready to start. +}; + +enum { + AUDIOH_STREAM_OP_NONE = 0, + AUDIOH_STREAM_OP_START, + AUDIOH_STREAM_OP_STOP +}; + +enum { + AUDIOH_CTRL_NONE = 0, + AUDIOH_CTRL_READ = 1, + AUDIOH_CTRL_READ_WRITE = 3 +}; + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + #define AUDIOH_MAX_RATE_SOURCES (2 * CFG_TUH_AUDIO_MAX_AS) + #else + #define AUDIOH_MAX_RATE_SOURCES TUH_AUDIO_STREAM_DIRECTION_COUNT + #endif + +// UAC1 stores one rate source per alternate setting. UAC2 alternate settings +// that reference the same Clock Source share one rate source. +typedef struct { + uint32_t sample_rate[CFG_TUH_AUDIO_MAX_SAM_FREQ]; + uint8_t control_id; // UAC1 endpoint address or UAC2 Clock Source ID. + uint8_t sample_rate_count; + uint8_t frequency_access; +} audioh_rate_source_t; + +// Properties shared by every sampling frequency of one AS alternate setting. +typedef struct { + uint16_t ep_size; + uint8_t itf_num; + uint8_t alt_setting; + uint8_t ep_addr; + uint8_t ep_interval; + uint8_t ep_attr; // Synchronization and usage fields from bmAttributes. + uint8_t format; + uint8_t channels; + uint8_t terminal_id; + uint8_t rate_source_idx; + uint8_t rate_count; +} audioh_as_config_t; + +// Explicit-feedback endpoint associated with a playback alternate setting. +typedef struct { + uint8_t ep_addr; + uint8_t ep_size; + uint8_t ep_interval; + uint8_t ep_attr; +} audioh_feedback_ep_t; + +typedef struct { + audioh_feedback_ep_t feedback[CFG_TUH_AUDIO_MAX_AS]; + + // Packet rates use Q16.16 audio frames per data-endpoint poll interval. The + // scheduler snapshots target_frames_q16 once per packet and retains rem_acc, + // which integrates fractional frames across feedback updates. + uint32_t nominal_frames_q16; + uint32_t target_frames_q16; + uint16_t feedback_min_frames; + uint16_t feedback_max_frames; + uint16_t rem_acc; + bool feedback_opened; +} audioh_playback_t; + +// Control-transfer bookkeeping; transfer payloads are stored in audioh_epbuf_t. +typedef struct { + tuh_xfer_cb_t complete_cb; + uintptr_t user_data; + void *value; + union { + struct { + uint8_t width; + uint8_t value_type; + uint8_t channel; + uint8_t last_channel; + } control; + struct { + uint8_t stream_idx; + uint8_t range_step; + } mount; + } fu; + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + struct { + uint8_t rate_source_idx; + bool read_cur; + } clock; + #endif + bool fu_busy; +} audioh_ctrl_state_t; + +// One logical capture or playback stream. +typedef struct { + // Identity is initialized once and preserved when the stream is reset. + uint8_t idx; + uint8_t stream_idx; + tusb_dir_t dir; // TUSB_DIR_IN is capture; TUSB_DIR_OUT is playback. + + // Device address, or zero while this stream slot is unused. + uint8_t daddr; + + // Configurations discovered during enumeration. + uint8_t as_count; + uint8_t config_count; + audioh_as_config_t as[CFG_TUH_AUDIO_MAX_AS]; + + // Selected configuration and runtime state. + uint8_t active_config; // Index in the flattened public configuration list. + uint8_t active_as; + uint8_t active_rate; + uint8_t state; + uint8_t operation; + bool running; + + // Directly associated Feature Unit, or zero when none is usable. + uint8_t feature_unit_id; + uint8_t mute_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. + uint16_t frame_bytes; + + // The FIFO decouples application I/O from isochronous transfers. ep_buf is + // assigned during driver initialization and the endpoint during configure. + tu_edpt_stream_t edpt; + uint8_t ff_buf[CFG_TUH_AUDIO_STREAM_BUFSIZE]; +} tuh_audio_stream_t; + +// State owned by one Audio Control interface. +typedef struct { + uint8_t daddr; // Device address, or zero for a free instance. + uint8_t ac_itf_num; + uint8_t protocol; + uint8_t stream_count; + uint8_t rate_source_count; + bool mounted; + + audioh_rate_source_t rate_source[AUDIOH_MAX_RATE_SOURCES]; + + // Public stream indices are assigned in playback-then-capture order. + tuh_audio_stream_t out_stream; + tuh_audio_stream_t in_stream; + audioh_playback_t playback; + audioh_ctrl_state_t ctrl; +} audioh_interface_t; + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + #define AUDIOH_CLOCK_RANGE_BUFSIZE (2 + 12 * CFG_TUH_AUDIO_MAX_SAM_FREQ) + #endif + +typedef struct { + // Clock discovery finishes before mount, so its buffer can be reused by + // runtime sampling-frequency and Feature Unit requests. + union { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + TUH_EPBUF_DEF(clock_range, AUDIOH_CLOCK_RANGE_BUFSIZE); + #endif + struct { + TUH_EPBUF_DEF(rate_ctrl, 4); + TUH_EPBUF_DEF(fu_ctrl, 8); + } runtime; + } control; + // Feedback transfers may overlap runtime control transfers, so the feedback buffer is separate. + TUH_EPBUF_DEF(feedback, 4); + TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); + TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); +} audioh_epbuf_t; + +static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; + +CFG_TUH_MEM_SECTION static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX]; + +//--------------------------------------------------------------------+ +// WEAK APPLICATION CALLBACKS +//--------------------------------------------------------------------+ + +TU_ATTR_WEAK void tuh_audio_descriptor_cb(uint8_t idx, const tuh_audio_descriptor_cb_t *desc_cb_data) { + (void)idx; + (void)desc_cb_data; +} + +TU_ATTR_WEAK void tuh_audio_mount_cb(uint8_t idx) { + (void)idx; +} + +TU_ATTR_WEAK void tuh_audio_umount_cb(uint8_t idx) { + (void)idx; +} + +TU_ATTR_WEAK void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { + (void)idx; + (void)stream_idx; + (void)xferred_bytes; +} + +TU_ATTR_WEAK void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { + (void)idx; + (void)stream_idx; + (void)xferred_bytes; +} + +TU_ATTR_WEAK void tuh_audio_event_cb(uint8_t idx, uint8_t stream_idx, tuh_audio_event_t event, + tusb_xfer_result_t result) { + (void)idx; + (void)stream_idx; + (void)event; + (void)result; +} + +//--------------------------------------------------------------------+ +// HELPERS +//--------------------------------------------------------------------+ + +TU_ATTR_ALWAYS_INLINE static inline uint8_t *audioh_rate_ctrl(audioh_epbuf_t *epbuf) { + return epbuf->control.runtime.rate_ctrl; +} + +TU_ATTR_ALWAYS_INLINE static inline uint8_t *audioh_fu_ctrl(audioh_epbuf_t *epbuf) { + return epbuf->control.runtime.fu_ctrl; +} + +TU_ATTR_ALWAYS_INLINE static inline uint8_t find_new_audio_index(void) { + for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { + if (_audioh_itf[idx].daddr == 0) { + return idx; + } + } + return TUSB_INDEX_INVALID_8; +} + +static bool audioh_desc_valid(const uint8_t *p_desc, const uint8_t *desc_end, uint8_t min_len) { + if (p_desc >= desc_end) { + return false; + } + + const size_t remaining = (size_t)(desc_end - p_desc); + return TUH_VALIDATE_BASIC(remaining >= min_len) && TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= min_len) && + TUH_VALIDATE_BASIC(tu_desc_len(p_desc) <= remaining); +} + +static bool audioh_protocol_enabled(uint8_t protocol) { + switch (protocol) { + case AUDIO_INT_PROTOCOL_CODE_V1: + return (CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1) != 0; + case AUDIO_INT_PROTOCOL_CODE_V2: + return (CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2) != 0; + default: + return false; + } +} + +static tuh_audio_stream_t *audioh_get_stream(audioh_interface_t *p_audio, tusb_dir_t direction) { + return (direction == TUSB_DIR_IN) ? &p_audio->in_stream : &p_audio->out_stream; +} + +static tuh_audio_stream_t *audioh_get_stream_by_idx(audioh_interface_t *p_audio, uint8_t stream_idx) { + for (uint8_t i = 0; i < 2; i++) { + tuh_audio_stream_t *s = (i == 0) ? &p_audio->out_stream : &p_audio->in_stream; + if (s->as_count > 0 && s->stream_idx == stream_idx) { + return s; + } + } + return NULL; +} + +TU_ATTR_ALWAYS_INLINE static inline tuh_audio_stream_t *audioh_get_stream_by_idx_unchecked( + audioh_interface_t *p_audio, uint8_t stream_idx) { + return (p_audio->out_stream.stream_idx == stream_idx) ? &p_audio->out_stream : &p_audio->in_stream; +} + +TU_ATTR_ALWAYS_INLINE static inline audioh_playback_t *audioh_get_playback(const tuh_audio_stream_t *s) { + return &_audioh_itf[s->idx].playback; +} + +TU_ATTR_ALWAYS_INLINE static inline audioh_as_config_t *audioh_stream_active_as(tuh_audio_stream_t *s) { + return &s->as[s->active_as]; +} + +TU_ATTR_ALWAYS_INLINE static inline audioh_rate_source_t *audioh_as_rate_source(const tuh_audio_stream_t *s, + const audioh_as_config_t *as) { + return &_audioh_itf[s->idx].rate_source[as->rate_source_idx]; +} + +static bool audioh_as_rate_fits(const audioh_interface_t *p_audio, const tuh_audio_stream_t *stream, + const audioh_as_config_t *as, uint32_t sample_rate); + +static bool audioh_stream_resolve_config(const tuh_audio_stream_t *s, uint8_t config_idx, uint8_t *as_idx, + uint8_t *rate_idx) { + for (uint8_t i = 0; i < s->as_count; i++) { + if (config_idx < s->as[i].rate_count) { + const audioh_as_config_t *as = &s->as[i]; + const audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); + for (uint8_t source_rate_idx = 0; source_rate_idx < rate_source->sample_rate_count; source_rate_idx++) { + if (audioh_as_rate_fits(&_audioh_itf[s->idx], s, as, rate_source->sample_rate[source_rate_idx])) { + if (config_idx == 0) { + *as_idx = i; + *rate_idx = source_rate_idx; + return true; + } + config_idx--; + } + } + return false; + } + config_idx -= s->as[i].rate_count; + } + return false; +} + +static void audioh_stream_config_fill(const tuh_audio_stream_t *s, uint8_t as_idx, uint8_t rate_idx, + tuh_audio_stream_config_t *config) { + const audioh_as_config_t *as = &s->as[as_idx]; + const audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); + config->dir = (s->dir == TUSB_DIR_IN) ? TUH_AUDIO_STREAM_CAPTURE : TUH_AUDIO_STREAM_PLAYBACK; + config->format = (tuh_audio_format_t)as->format; + config->sample_rate = rate_source->sample_rate[rate_idx]; + config->channels = as->channels; +} + +static bool audioh_stream_config_get(const tuh_audio_stream_t *s, uint8_t config_idx, + tuh_audio_stream_config_t *config) { + uint8_t as_idx; + uint8_t rate_idx; + TU_VERIFY(audioh_stream_resolve_config(s, config_idx, &as_idx, &rate_idx), false); + + audioh_stream_config_fill(s, as_idx, rate_idx, config); + return true; +} + +static void audioh_stream_set_feature_unit(tuh_audio_stream_t *s, uint8_t unit_id, uint8_t mute_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) { + if (subslot_size == 1 && bit_resolution == 8) { + *format = TUH_AUDIO_FORMAT_S8; + } else if (subslot_size == 2 && bit_resolution == 16) { + *format = TUH_AUDIO_FORMAT_S16_LE; + } else if (subslot_size == 3 && bit_resolution == 24) { + *format = TUH_AUDIO_FORMAT_S24_3LE; + } else if (subslot_size == 4 && bit_resolution == 24) { + *format = TUH_AUDIO_FORMAT_S24_LE; + } else if (subslot_size == 4 && bit_resolution == 32) { + *format = TUH_AUDIO_FORMAT_S32_LE; + } else { + return false; + } + return true; +} + +// bInterval encodes 2^(bInterval-1) full-speed frames or high-speed microframes. +static uint32_t audioh_interval_us(uint8_t ep_interval, uint8_t daddr) { + const uint32_t unit_us = (tuh_speed_get(daddr) == TUSB_SPEED_HIGH) ? 125u : 1000u; + return ((uint32_t)1u << (ep_interval - 1)) * unit_us; +} + +// Convert a nominal sample rate to Q16.16 frames per data endpoint poll +// interval. Round to the nearest representable value to preserve common +// fractional rates such as 44.1 frames/ms. +static uint32_t audioh_nominal_frames_q16(uint32_t sample_rate, uint8_t ep_interval, uint8_t daddr) { + const uint64_t numerator = (uint64_t)sample_rate * audioh_interval_us(ep_interval, daddr) * 65536u; + return (uint32_t)((numerator + 500000u) / 1000000u); +} + +// 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_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); +} + +static void audioh_playback_reset(audioh_playback_t *playback) { + tu_memclr(playback, sizeof(*playback)); +} + +static tuh_audio_stream_t *audioh_find_stream(uint8_t dev_addr, uint8_t ep_addr) { + for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + for (uint8_t s = 0; s < 2; s++) { + tuh_audio_stream_t *stream = (s == 0) ? &p_audio->in_stream : &p_audio->out_stream; + if (stream->daddr == dev_addr && stream->active_config != TUSB_INDEX_INVALID_8) { + const audioh_as_config_t *as = audioh_stream_active_as(stream); + if (as->ep_addr == ep_addr) { + return stream; + } + const uint8_t feedback_ep = p_audio->playback.feedback[stream->active_as].ep_addr; + if (stream->dir == TUSB_DIR_OUT && feedback_ep != 0 && feedback_ep == ep_addr) { + return stream; + } + } + } + } + return NULL; +} + +//--------------------------------------------------------------------+ +// PACKET SCHEDULER +//--------------------------------------------------------------------+ + +static void audioh_stream_xfer_failed(tuh_audio_stream_t *s, tusb_xfer_result_t result); + +static bool audioh_stream_feedback_xfer(tuh_audio_stream_t *s) { + const audioh_feedback_ep_t *feedback = &audioh_get_playback(s)->feedback[s->active_as]; + TU_VERIFY(usbh_edpt_claim(s->daddr, feedback->ep_addr), false); + return usbh_edpt_xfer(s->daddr, feedback->ep_addr, _audioh_epbuf[s->idx].feedback, feedback->ep_size); +} + +static bool audioh_stream_capture_xfer(tuh_audio_stream_t *s) { + const audioh_as_config_t *as = audioh_stream_active_as(s); + TU_VERIFY(usbh_edpt_claim(s->daddr, as->ep_addr), false); + return usbh_edpt_xfer(s->daddr, as->ep_addr, s->edpt.ep_buf, as->ep_size); +} + +static bool audioh_stream_playback_xfer(tuh_audio_stream_t *s) { + const audioh_as_config_t *as = audioh_stream_active_as(s); + audioh_playback_t *playback = audioh_get_playback(s); + TU_VERIFY(usbh_edpt_claim(s->daddr, as->ep_addr), false); + + // Use one target for the entire packet calculation. Retaining the fractional + // remainder makes the scheduled total follow the sum of changing feedback + // values with less than one frame of quantization error. + const uint32_t target_q16 = playback->target_frames_q16; + uint32_t frames = target_q16 >> 16; + const uint32_t fraction = target_q16 & 0xFFFFu; + uint32_t next_rem_acc = playback->rem_acc + fraction; + if (next_rem_acc >= 65536u) { + next_rem_acc -= 65536u; + frames++; + } + + const uint16_t bytes = (uint16_t)(frames * s->frame_bytes); + if (tu_fifo_count(&s->edpt.ff) < bytes) { + // Isochronous OUT must continue at every interval. Send silence until a + // complete packet is queued, leaving any partial packet in the FIFO. + tu_memclr(s->edpt.ep_buf, bytes); + } else { + tu_fifo_read_n(&s->edpt.ff, s->edpt.ep_buf, bytes); + } + + if (!usbh_edpt_xfer(s->daddr, as->ep_addr, s->edpt.ep_buf, bytes)) { + return false; + } + playback->rem_acc = (uint16_t)next_rem_acc; + return true; +} + +//--------------------------------------------------------------------+ +// STREAM CONFIGURATION +//--------------------------------------------------------------------+ + +static bool audioh_stream_close_ep(tuh_audio_stream_t *s) { + audioh_playback_t *playback = (s->dir == TUSB_DIR_OUT) ? audioh_get_playback(s) : NULL; + if (playback != NULL && playback->feedback_opened) { + const uint8_t fb_ep_addr = playback->feedback[s->active_as].ep_addr; + if (!tuh_edpt_close(s->daddr, fb_ep_addr)) { + TU_LOG_DRV(" AUDIO close feedback endpoint failed: addr=%u ep=%02x\r\n", s->daddr, fb_ep_addr); + return false; + } + playback->feedback_opened = false; + } + + if (!tu_edpt_stream_is_opened(&s->edpt)) { + return true; + } + + const uint8_t ep_addr = s->edpt.ep_addr; + if (!tuh_edpt_close(s->daddr, ep_addr)) { + TU_LOG_DRV(" AUDIO close endpoint failed: addr=%u ep=%02x\r\n", s->daddr, ep_addr); + return false; + } + + tu_edpt_stream_close(&s->edpt); + return true; +} + +static void audioh_stream_fail(tuh_audio_stream_t *s) { + (void)audioh_stream_close_ep(s); + s->state = STREAM_STATE_IDLE; + s->active_config = TUSB_INDEX_INVALID_8; + s->active_as = TUSB_INDEX_INVALID_8; + s->active_rate = TUSB_INDEX_INVALID_8; + s->operation = AUDIOH_STREAM_OP_NONE; + s->running = false; +} + +static void audioh_stream_stop_xfers(tuh_audio_stream_t *s) { + s->running = false; + if (s->dir == TUSB_DIR_OUT) { + audioh_playback_t *playback = audioh_get_playback(s); + playback->target_frames_q16 = playback->nominal_frames_q16; + playback->rem_acc = 0; + } + tu_edpt_stream_clear(&s->edpt); +} + +static void audioh_stream_xfer_failed(tuh_audio_stream_t *s, tusb_xfer_result_t result) { + audioh_stream_stop_xfers(s); + tuh_audio_event_cb(s->idx, s->stream_idx, TUH_AUDIO_EVENT_XFER_FAILED, result); +} + +static bool audioh_stream_set_freq(tuh_audio_stream_t *s, tuh_xfer_cb_t complete_cb) { + const audioh_as_config_t *as = audioh_stream_active_as(s); + const audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); + const uint32_t sample_rate = rate_source->sample_rate[s->active_rate]; + uint8_t *ctrl = audioh_rate_ctrl(&_audioh_epbuf[s->idx]); + tusb_control_request_t request = {0}; + + ctrl[0] = (uint8_t)(sample_rate & 0xFF); + ctrl[1] = (uint8_t)((sample_rate >> 8) & 0xFF); + ctrl[2] = (uint8_t)((sample_rate >> 16) & 0xFF); + switch (_audioh_itf[s->idx].protocol) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + case AUDIO_INT_PROTOCOL_CODE_V1: + request.bmRequestType_bit.recipient = TUSB_REQ_RCPT_ENDPOINT; + request.bmRequestType_bit.type = TUSB_REQ_TYPE_CLASS; + request.bmRequestType_bit.direction = TUSB_DIR_OUT; + request.bRequest = AUDIO10_CS_REQ_SET_CUR; + request.wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)); + request.wIndex = tu_htole16(rate_source->control_id); + request.wLength = 3; + break; + #endif + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + case AUDIO_INT_PROTOCOL_CODE_V2: + request.bmRequestType_bit.recipient = TUSB_REQ_RCPT_INTERFACE; + request.bmRequestType_bit.type = TUSB_REQ_TYPE_CLASS; + request.bmRequestType_bit.direction = TUSB_DIR_OUT; + request.bRequest = AUDIO20_CS_REQ_CUR; + request.wValue = tu_htole16(tu_u16(AUDIO20_CS_CTRL_SAM_FREQ, 0)); + request.wIndex = tu_htole16(tu_u16(rate_source->control_id, _audioh_itf[s->idx].ac_itf_num)); + request.wLength = 4; + ctrl[3] = (uint8_t)(sample_rate >> 24); + break; + #endif + default: + return false; + } + + tuh_xfer_t xfer = {.daddr = s->daddr, + .ep_addr = 0, + .setup = &request, + .buffer = ctrl, + .complete_cb = complete_cb, + .user_data = (uintptr_t)s}; + return tuh_control_xfer(&xfer); +} + +static bool audioh_stream_open_ep(tuh_audio_stream_t *s) { + const audioh_as_config_t *as = audioh_stream_active_as(s); + + const tusb_desc_endpoint_t desc_ep = {.bLength = sizeof(tusb_desc_endpoint_t), + .bDescriptorType = TUSB_DESC_ENDPOINT, + .bEndpointAddress = as->ep_addr, + .bmAttributes = {.xfer = TUSB_XFER_ISOCHRONOUS, + .sync = (as->ep_attr >> 2) & 0x03u, + .usage = (as->ep_attr >> 4) & 0x03u}, + .wMaxPacketSize = tu_htole16(as->ep_size), + .bInterval = as->ep_interval}; + + if (!tuh_edpt_open(s->daddr, &desc_ep)) { + TU_LOG_DRV(" AUDIO open endpoint failed: addr=%u ep=%02x\r\n", s->daddr, as->ep_addr); + audioh_stream_fail(s); + return false; + } + + // Bind the transfer helper to the selected endpoint and empty its FIFO. + const uint16_t xfer_len = (s->dir == TUSB_DIR_IN) ? CFG_TUH_AUDIO_EPIN_BUFSIZE : CFG_TUH_AUDIO_EPOUT_BUFSIZE; + tu_edpt_stream_open(&s->edpt, s->daddr, &desc_ep, xfer_len); + tu_edpt_stream_clear(&s->edpt); + + if (s->dir == TUSB_DIR_OUT) { + audioh_playback_t *playback = audioh_get_playback(s); + const audioh_feedback_ep_t *feedback = &playback->feedback[s->active_as]; + if (feedback->ep_addr != 0) { + const tusb_desc_endpoint_t desc_fb = {.bLength = sizeof(tusb_desc_endpoint_t), + .bDescriptorType = TUSB_DESC_ENDPOINT, + .bEndpointAddress = feedback->ep_addr, + .bmAttributes = {.xfer = TUSB_XFER_ISOCHRONOUS, + .sync = (feedback->ep_attr >> 2) & 0x03u, + .usage = (feedback->ep_attr >> 4) & 0x03u}, + .wMaxPacketSize = tu_htole16(feedback->ep_size), + .bInterval = feedback->ep_interval}; + if (!tuh_edpt_open(s->daddr, &desc_fb)) { + TU_LOG_DRV(" AUDIO open feedback endpoint failed: addr=%u ep=%02x\r\n", s->daddr, feedback->ep_addr); + audioh_stream_fail(s); + return false; + } + playback->feedback_opened = true; + } + } + + s->state = STREAM_STATE_READY; + return true; +} + +//--------------------------------------------------------------------+ +// USB HOST CLASS DRIVER +//--------------------------------------------------------------------+ +bool audioh_init(void) { + tu_memclr(&_audioh_itf, sizeof(_audioh_itf)); + + for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { + tuh_audio_stream_t *in = &_audioh_itf[idx].in_stream; + tuh_audio_stream_t *out = &_audioh_itf[idx].out_stream; + + in->idx = idx; + in->dir = TUSB_DIR_IN; + out->idx = idx; + out->dir = TUSB_DIR_OUT; + + TU_VERIFY(tu_edpt_stream_init(&in->edpt, true, false, true, in->ff_buf, CFG_TUH_AUDIO_STREAM_BUFSIZE, + _audioh_epbuf[idx].epin)); + TU_VERIFY(tu_edpt_stream_init(&out->edpt, true, true, false, out->ff_buf, CFG_TUH_AUDIO_STREAM_BUFSIZE, + _audioh_epbuf[idx].epout)); + + audioh_stream_reset(in); + audioh_stream_reset(out); + audioh_playback_reset(&_audioh_itf[idx].playback); + } + return true; +} + +bool audioh_deinit(void) { + for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { + tu_edpt_stream_deinit(&_audioh_itf[idx].in_stream.edpt); + tu_edpt_stream_deinit(&_audioh_itf[idx].out_stream.edpt); + } + return true; +} + +void audioh_close(uint8_t daddr) { + for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + if (p_audio->daddr != daddr) { + continue; + } + + TU_LOG_DRV(" AUDIO close addr = %u index = %u\r\n", daddr, idx); + if (p_audio->mounted) { + tuh_audio_umount_cb(idx); + } + + for (uint8_t s = 0; s < 2; s++) { + tuh_audio_stream_t *stream = (s == 0) ? &p_audio->in_stream : &p_audio->out_stream; + audioh_stream_reset(stream); + } + audioh_playback_reset(&p_audio->playback); + + // A disconnected device cannot complete its pending control request. + tu_memclr(&p_audio->ctrl, sizeof(p_audio->ctrl)); + + p_audio->stream_count = 0; + p_audio->daddr = 0; + p_audio->protocol = 0; + p_audio->rate_source_count = 0; + p_audio->mounted = false; + } +} + +static void audioh_feedback_received(tuh_audio_stream_t *s, uint32_t xferred_bytes) { + const uint8_t *fb = _audioh_epbuf[s->idx].feedback; + audioh_playback_t *playback = audioh_get_playback(s); + uint32_t feedback_q16; + if (xferred_bytes == 3) { + // Three-byte feedback is Q10.14; the scheduler uses Q16.16 throughout. + feedback_q16 = ((uint32_t)fb[0] | ((uint32_t)fb[1] << 8) | ((uint32_t)fb[2] << 16)) << 2; + } else if (xferred_bytes == 4) { + feedback_q16 = (uint32_t)fb[0] | ((uint32_t)fb[1] << 8) | ((uint32_t)fb[2] << 16) | ((uint32_t)fb[3] << 24); + } else { + TU_LOG_DRV(" AUDIO invalid feedback length: %lu\r\n", (unsigned long)xferred_bytes); + return; + } + + const uint32_t feedback_min_q16 = (uint32_t)playback->feedback_min_frames << 16; + const uint32_t feedback_max_q16 = (uint32_t)playback->feedback_max_frames << 16; + if (feedback_q16 < feedback_min_q16 || feedback_q16 > feedback_max_q16) { + TU_LOG_DRV(" AUDIO feedback out of range: 0x%08lx\r\n", (unsigned long)feedback_q16); + return; + } + + // Feedback is measured per USB frame or microframe. Scale it to the data + // endpoint's polling interval. + const audioh_as_config_t *as = audioh_stream_active_as(s); + const uint64_t target_q16_64 = (uint64_t)feedback_q16 << (as->ep_interval - 1u); + if (target_q16_64 > UINT32_MAX) { + return; + } + + const uint32_t target_q16 = (uint32_t)target_q16_64; + const uint64_t max_bytes = (((uint64_t)target_q16 + 0xFFFFu) >> 16) * s->frame_bytes; + if (max_bytes == 0 || max_bytes > as->ep_size || max_bytes > CFG_TUH_AUDIO_EPOUT_BUFSIZE || + max_bytes > CFG_TUH_AUDIO_STREAM_BUFSIZE) { + TU_LOG_DRV(" AUDIO feedback exceeds playback packet capacity: 0x%08lx\r\n", (unsigned long)feedback_q16); + return; + } + + // Host-class callbacks run serially. The playback scheduler snapshots this + // target before calculating a packet, so an update cannot split a packet + // calculation across two rates. + playback->target_frames_q16 = target_q16; +} + +bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { + tuh_audio_stream_t *s = audioh_find_stream(dev_addr, ep_addr); + if (s == NULL) { + return false; + } + + // Stopping one endpoint does not cancel every transfer that may already be + // in flight (for example, a playback data and feedback pair). Ignore those + // completions after the stream has stopped. + if (!s->running) { + return true; + } + + // Failed, stalled, and aborted transfers do not carry valid audio data. + if (result != XFER_RESULT_SUCCESS) { + TU_LOG_DRV(" AUDIO transfer failed: addr=%u ep=%02x result=%u\r\n", dev_addr, ep_addr, result); + audioh_stream_xfer_failed(s, (tusb_xfer_result_t)result); + return true; + } + + const uint8_t feedback_ep = audioh_get_playback(s)->feedback[s->active_as].ep_addr; + if (s->dir == TUSB_DIR_OUT && feedback_ep != 0 && ep_addr == feedback_ep) { + audioh_feedback_received(s, xferred_bytes); + if (!audioh_stream_feedback_xfer(s)) { + audioh_stream_xfer_failed(s, XFER_RESULT_FAILED); + } + return true; + } + + if (s->dir == TUSB_DIR_IN) { + // Queue whole capture frames, notify the application, then re-arm. + const uint16_t bytes = (uint16_t)(xferred_bytes - (xferred_bytes % s->frame_bytes)); + if (bytes > 0) { + tu_fifo_write_n(&s->edpt.ff, s->edpt.ep_buf, bytes); + } + tuh_audio_capture_cb(s->idx, s->stream_idx, (uint16_t)xferred_bytes); + if (s->running && !audioh_stream_capture_xfer(s)) { + audioh_stream_xfer_failed(s, XFER_RESULT_FAILED); + } + } else { + // Notify the application before requesting the next playback packet. + tuh_audio_playback_cb(s->idx, s->stream_idx, (uint16_t)xferred_bytes); + if (s->running && !audioh_stream_playback_xfer(s)) { + audioh_stream_xfer_failed(s, XFER_RESULT_FAILED); + } + } + return true; +} + +//--------------------------------------------------------------------+ +// ENUMERATION +//--------------------------------------------------------------------+ + +typedef struct { + uint8_t id; + uint8_t source_id; + uint8_t clock_id; + uint8_t stream_dir; +} audioh_terminal_info_t; + +typedef struct { + uint8_t id; + uint8_t source_id; + uint8_t mute_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 { + uint8_t id; + uint8_t frequency_access; +} audioh_clock_info_t; + +typedef struct { + const uint8_t *desc_start; + const uint8_t *desc_end; +} audioh_ac_desc_range_t; + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 +static uint8_t audioh_uac2_control_access(uint32_t controls, uint8_t position) { + const uint8_t access = (uint8_t)((controls >> position) & 0x03u); + return (access == AUDIOH_CTRL_READ || access == AUDIOH_CTRL_READ_WRITE) ? access : AUDIOH_CTRL_NONE; +} + #endif + +static bool audioh_as_rate_fits(const audioh_interface_t *p_audio, const tuh_audio_stream_t *stream, + const audioh_as_config_t *as, uint32_t sample_rate) { + const uint32_t frame_bytes = (uint32_t)as->channels * tuh_audio_format_bytes((tuh_audio_format_t)as->format); + const uint16_t epbuf_size = (stream->dir == TUSB_DIR_IN) ? CFG_TUH_AUDIO_EPIN_BUFSIZE : CFG_TUH_AUDIO_EPOUT_BUFSIZE; + const uint64_t frames_numerator = (uint64_t)sample_rate * audioh_interval_us(as->ep_interval, p_audio->daddr); + const uint64_t max_frames = (frames_numerator + 999999u) / 1000000u; + const uint64_t packet_bytes = max_frames * frame_bytes; + + return packet_bytes > 0 && packet_bytes <= as->ep_size && + (stream->dir != TUSB_DIR_OUT || (packet_bytes <= epbuf_size && packet_bytes <= CFG_TUH_AUDIO_STREAM_BUFSIZE)); +} + +static bool audioh_ac_entity_valid(const audioh_interface_t *p_audio, const uint8_t *p_desc) { + switch (p_audio->protocol) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + case AUDIO_INT_PROTOCOL_CODE_V1: { + switch (tu_desc_subtype(p_desc)) { + case AUDIO10_CS_AC_INTERFACE_INPUT_TERMINAL: + return TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio10_desc_input_terminal_t)); + case AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL: + return TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio10_desc_output_terminal_t)); + 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]; + 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; + } + } + #endif + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + case AUDIO_INT_PROTOCOL_CODE_V2: + switch (tu_desc_subtype(p_desc)) { + case AUDIO20_CS_AC_INTERFACE_INPUT_TERMINAL: + return TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio20_desc_input_terminal_t)); + 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) && + 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: + return true; + } + #endif + default: + return false; + } +} + +static bool audioh_ac_terminal_find(const audioh_interface_t *p_audio, const audioh_ac_desc_range_t *range, uint8_t id, + audioh_terminal_info_t *info) { + for (const uint8_t *p_desc = range->desc_start; p_desc < range->desc_end; p_desc = tu_desc_next(p_desc)) { + if (tu_desc_type(p_desc) != TUSB_DESC_CS_INTERFACE || tu_desc_len(p_desc) < 4 || p_desc[3] != id) { + continue; + } + 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_INPUT_TERMINAL) { + const audio10_desc_input_terminal_t *terminal = (const audio10_desc_input_terminal_t *)p_desc; + if (tu_le16toh(terminal->wTerminalType) == AUDIO_TERM_TYPE_USB_STREAMING) { + *info = (audioh_terminal_info_t){.id = id, .stream_dir = TUSB_DIR_OUT}; + return true; + } + } else if (tu_desc_subtype(p_desc) == AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL) { + const audio10_desc_output_terminal_t *terminal = (const audio10_desc_output_terminal_t *)p_desc; + if (tu_le16toh(terminal->wTerminalType) == AUDIO_TERM_TYPE_USB_STREAMING) { + *info = (audioh_terminal_info_t){.id = id, .source_id = terminal->bSourceID, .stream_dir = TUSB_DIR_IN}; + return true; + } + } + 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_INPUT_TERMINAL) { + const audio20_desc_input_terminal_t *terminal = (const audio20_desc_input_terminal_t *)p_desc; + if (tu_le16toh(terminal->wTerminalType) == AUDIO_TERM_TYPE_USB_STREAMING) { + *info = (audioh_terminal_info_t){.id = id, .clock_id = terminal->bCSourceID, .stream_dir = TUSB_DIR_OUT}; + return true; + } + } else if (tu_desc_subtype(p_desc) == AUDIO20_CS_AC_INTERFACE_OUTPUT_TERMINAL) { + const audio20_desc_output_terminal_t *terminal = (const audio20_desc_output_terminal_t *)p_desc; + if (tu_le16toh(terminal->wTerminalType) == AUDIO_TERM_TYPE_USB_STREAMING) { + *info = (audioh_terminal_info_t){.id = id, + .source_id = terminal->bSourceID, + .clock_id = terminal->bCSourceID, + .stream_dir = TUSB_DIR_IN}; + return true; + } + } + break; + #endif + default: + return false; + } + } + return false; +} + +static bool audioh_ac_feature_unit_parse(const audioh_interface_t *p_audio, const uint8_t *p_desc, + audioh_fu_info_t *info) { + 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) { + 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) { + 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: + 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 +static bool audioh_ac_clock_find(const audioh_ac_desc_range_t *range, uint8_t id, audioh_clock_info_t *info) { + for (const uint8_t *p_desc = range->desc_start; p_desc < range->desc_end; p_desc = tu_desc_next(p_desc)) { + if (tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE && + tu_desc_subtype(p_desc) == AUDIO20_CS_AC_INTERFACE_CLOCK_SOURCE && p_desc[3] == id) { + const audio20_desc_clock_source_t *clock = (const audio20_desc_clock_source_t *)p_desc; + *info = + (audioh_clock_info_t){.id = id, + .frequency_access = + audioh_uac2_control_access(clock->bmControls, AUDIO20_CLOCK_SOURCE_CTRL_CLK_FRQ_POS)}; + return true; + } + } + return false; +} + #endif + +static void audioh_link_feature_units(audioh_interface_t *p_audio, const audioh_ac_desc_range_t *range) { + for (uint8_t direction = TUSB_DIR_OUT; direction <= TUSB_DIR_IN; direction++) { + tuh_audio_stream_t *stream = audioh_get_stream(p_audio, (tusb_dir_t)direction); + if (stream->as_count == 0) { + continue; + } + audioh_terminal_info_t terminal; + if (!audioh_ac_terminal_find(p_audio, range, stream->as[0].terminal_id, &terminal) || + terminal.stream_dir != direction) { + continue; + } + for (const uint8_t *p_desc = range->desc_start; p_desc < range->desc_end; p_desc = tu_desc_next(p_desc)) { + audioh_fu_info_t fu; + if (!audioh_ac_feature_unit_parse(p_audio, p_desc, &fu)) { + continue; + } + 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_master_access, fu.channels, + fu.volume_range_channel, fu.volume_all_channels_writable); + break; + } + } + } +} + +typedef struct { + uint32_t format_bitmap; + uint16_t format_tag; + const uint8_t *sample_rate_data; + uint8_t terminal_id; + uint8_t format_type; + uint8_t channels; + uint8_t subslot_size; + uint8_t bit_resolution; + uint8_t sample_rate_count; +} audioh_as_class_info_t; + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 +static bool audioh_uac1_parse_as_interface(const uint8_t *p_desc, audioh_as_class_info_t *info) { + switch (tu_desc_subtype(p_desc)) { + case AUDIO10_CS_AS_INTERFACE_AS_GENERAL: { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio10_desc_cs_as_interface_t)), false); + const audio10_desc_cs_as_interface_t *general = (const audio10_desc_cs_as_interface_t *)p_desc; + info->terminal_id = general->bTerminalLink; + info->format_tag = tu_le16toh(general->wFormatTag); + break; + } + case AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE: + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 8), false); + info->format_type = p_desc[3]; + if (info->format_type != AUDIO10_FORMAT_TYPE_I) { + break; + } + info->channels = p_desc[4]; + info->subslot_size = p_desc[5]; + info->bit_resolution = p_desc[6]; + info->sample_rate_count = 0; + info->sample_rate_data = NULL; + if (p_desc[7] > 0) { + TU_VERIFY(TUH_VALIDATE_BASIC(p_desc[7] <= (tu_desc_len(p_desc) - 8u) / 3u), false); + info->sample_rate_count = TU_MIN(p_desc[7], CFG_TUH_AUDIO_MAX_SAM_FREQ); + info->sample_rate_data = &p_desc[8]; + } + break; + default: + break; + } + return true; +} + #endif + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 +static bool audioh_uac2_parse_as_interface(const uint8_t *p_desc, audioh_as_class_info_t *info) { + switch (tu_desc_subtype(p_desc)) { + case AUDIO20_CS_AS_INTERFACE_AS_GENERAL: { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio20_desc_cs_as_interface_t)), false); + const audio20_desc_cs_as_interface_t *general = (const audio20_desc_cs_as_interface_t *)p_desc; + info->terminal_id = general->bTerminalLink; + info->format_type = general->bFormatType; + info->format_bitmap = tu_le32toh(general->bmFormats); + info->channels = general->bNrChannels; + break; + } + case AUDIO20_CS_AS_INTERFACE_FORMAT_TYPE: { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio20_desc_type_I_format_t)), false); + const audio20_desc_type_I_format_t *format = (const audio20_desc_type_I_format_t *)p_desc; + if (format->bFormatType == AUDIO20_FORMAT_TYPE_I) { + info->subslot_size = format->bSubslotSize; + info->bit_resolution = format->bBitResolution; + } + break; + } + default: + break; + } + return true; +} + #endif + +static bool audioh_parse_as_interface(audioh_interface_t *p_audio, const uint8_t *p_desc, + audioh_as_class_info_t *info) { + switch (p_audio->protocol) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + case AUDIO_INT_PROTOCOL_CODE_V1: + return audioh_uac1_parse_as_interface(p_desc, info); + #endif + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + case AUDIO_INT_PROTOCOL_CODE_V2: + return audioh_uac2_parse_as_interface(p_desc, info); + #endif + default: + return false; + } +} + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 +static int8_t audioh_uac2_rate_source_get(audioh_interface_t *p_audio, const audioh_ac_desc_range_t *range, + const audioh_terminal_info_t *terminal) { + if (terminal->clock_id == 0) { + return -1; + } + audioh_clock_info_t clock; + if (!audioh_ac_clock_find(range, terminal->clock_id, &clock) || clock.frequency_access == AUDIOH_CTRL_NONE) { + return -1; + } + for (uint8_t i = 0; i < p_audio->rate_source_count; i++) { + if (p_audio->rate_source[i].control_id == clock.id) { + return (int8_t)i; + } + } + if (p_audio->rate_source_count >= AUDIOH_MAX_RATE_SOURCES) { + return -1; + } + const uint8_t idx = p_audio->rate_source_count++; + p_audio->rate_source[idx] = + (audioh_rate_source_t){.control_id = clock.id, .frequency_access = clock.frequency_access}; + return (int8_t)idx; +} + #endif + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 +static bool audioh_uac1_rates_store(const audioh_interface_t *p_audio, const tuh_audio_stream_t *stream, + audioh_as_config_t *as, const audioh_as_class_info_t *info, + audioh_rate_source_t *rate_source) { + for (uint8_t i = 0; i < info->sample_rate_count; i++) { + const uint8_t *rate_data = &info->sample_rate_data[i * 3u]; + const uint32_t sample_rate = + (uint32_t)rate_data[0] | ((uint32_t)rate_data[1] << 8) | ((uint32_t)rate_data[2] << 16); + if (sample_rate == 0 || !audioh_as_rate_fits(p_audio, stream, as, sample_rate)) { + continue; + } + + const uint8_t rate_idx = rate_source->sample_rate_count++; + rate_source->sample_rate[rate_idx] = sample_rate; + as->rate_count++; + } + return as->rate_count > 0; +} + #endif + +// Parse one AS alternate setting and return the next interface descriptor. +// Supported configurations are appended to the stream matching its endpoint. +static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ac_desc_range_t *ac_desc, + const tusb_desc_interface_t *desc_itf, const uint8_t *p_desc, + const uint8_t *desc_end) { + const uint8_t itf_num = desc_itf->bInterfaceNumber; + const uint8_t alt = desc_itf->bAlternateSetting; + + p_desc = tu_desc_next(p_desc); + + // Alternate setting zero is the zero-bandwidth setting, not a configuration. + if (alt == 0 || desc_itf->bNumEndpoints == 0) { + while (p_desc < desc_end) { + TU_VERIFY(audioh_desc_valid(p_desc, desc_end, 2), NULL); + if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { + break; + } + p_desc = tu_desc_next(p_desc); + } + return p_desc; + } + + audioh_as_class_info_t class_info = {0}; + + // Retain one data endpoint and, for playback, one explicit-feedback endpoint. + // An implicit-feedback IN endpoint remains the data endpoint of its own AS + // interface and is therefore exposed as a capture stream. + typedef struct { + uint8_t ep_addr; + uint16_t ep_size; + uint8_t ep_interval; + uint8_t ep_attr; + bool sam_freq_ctrl; + } audioh_ep_info_t; + audioh_ep_info_t ep_info = {0}; + audioh_ep_info_t fb_info = {0}; + bool has_data_ep = false; + bool has_feedback_ep = false; + + while (p_desc < desc_end) { + TU_VERIFY(audioh_desc_valid(p_desc, desc_end, 2), NULL); + if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { + break; + } + + switch (tu_desc_type(p_desc)) { + case TUSB_DESC_CS_INTERFACE: { + TU_VERIFY(audioh_desc_valid(p_desc, desc_end, 3), NULL); + TU_VERIFY(audioh_parse_as_interface(p_audio, p_desc, &class_info), NULL); + break; + } + case TUSB_DESC_CS_ENDPOINT: { + TU_VERIFY(audioh_desc_valid(p_desc, desc_end, 3), NULL); + if (p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V1 && + tu_desc_subtype(p_desc) == AUDIO10_CS_EP_SUBTYPE_GENERAL) { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 4), NULL); + const audio10_desc_cs_as_iso_data_ep_t *desc_ep = (const audio10_desc_cs_as_iso_data_ep_t *)p_desc; + ep_info.sam_freq_ctrl = (desc_ep->bmAttributes & AUDIO10_CS_AS_ISO_DATA_EP_ATT_SAMPLING_FRQ) != 0; + } + break; + } + case TUSB_DESC_ENDPOINT: { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(tusb_desc_endpoint_t)), NULL); + const tusb_desc_endpoint_t *desc_endpoint = (const tusb_desc_endpoint_t *)p_desc; + if (desc_endpoint->bmAttributes.xfer != TUSB_XFER_ISOCHRONOUS) { + break; + } + + bool is_data_ep = false; + bool is_explicit_feedback = false; + // UAC1 distinguishes feedback by synchronization type; UAC2 uses the + // endpoint usage field. + switch (p_audio->protocol) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + case AUDIO_INT_PROTOCOL_CODE_V1: + is_data_ep = desc_endpoint->bmAttributes.sync != TUSB_ISO_EP_ATT_NO_SYNC; + is_explicit_feedback = tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN && !is_data_ep; + break; + #endif + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + case AUDIO_INT_PROTOCOL_CODE_V2: + is_data_ep = desc_endpoint->bmAttributes.usage == (TUSB_ISO_EP_ATT_DATA >> 4) || + desc_endpoint->bmAttributes.usage == (TUSB_ISO_EP_ATT_IMPLICIT_FB >> 4); + is_explicit_feedback = tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN && + desc_endpoint->bmAttributes.usage == (TUSB_ISO_EP_ATT_EXPLICIT_FB >> 4); + break; + #endif + default: + break; + } + + if (is_explicit_feedback) { + const uint16_t fb_ep_size = tu_edpt_packet_size(desc_endpoint); + if (has_feedback_ep || (fb_ep_size != 3 && fb_ep_size != 4)) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: invalid/extra feedback ep %02x ignored\r\n", itf_num, alt, + desc_endpoint->bEndpointAddress); + break; + } + + fb_info.ep_addr = desc_endpoint->bEndpointAddress; + fb_info.ep_size = fb_ep_size; + fb_info.ep_interval = desc_endpoint->bInterval; + if (fb_info.ep_interval == 0 || fb_info.ep_interval > 16) { + fb_info.ep_interval = 1; + } + fb_info.ep_attr = + (uint8_t)((desc_endpoint->bmAttributes.sync << 2) | (desc_endpoint->bmAttributes.usage << 4)); + has_feedback_ep = true; + break; + } + + if (is_data_ep) { + if (has_data_ep) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: extra data ep %02x ignored\r\n", itf_num, alt, + desc_endpoint->bEndpointAddress); + break; + } + + ep_info.ep_addr = desc_endpoint->bEndpointAddress; + ep_info.ep_size = tu_edpt_packet_size(desc_endpoint); + ep_info.ep_interval = desc_endpoint->bInterval; + // Isochronous bInterval is an exponent in the inclusive range 1..16. + if (ep_info.ep_interval == 0 || ep_info.ep_interval > 16) { + ep_info.ep_interval = 1; + } + ep_info.ep_attr = + (uint8_t)((desc_endpoint->bmAttributes.sync << 2) | (desc_endpoint->bmAttributes.usage << 4)); + has_data_ep = true; + } + break; + } + default: + break; + } + p_desc = tu_desc_next(p_desc); + } + + if (!has_data_ep) { + return p_desc; + } + + bool pcm_supported = false; + switch (p_audio->protocol) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + case AUDIO_INT_PROTOCOL_CODE_V1: + pcm_supported = + class_info.format_type == AUDIO10_FORMAT_TYPE_I && class_info.format_tag == AUDIO10_DATA_FORMAT_TYPE_I_PCM; + break; + #endif + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + case AUDIO_INT_PROTOCOL_CODE_V2: + pcm_supported = class_info.format_type == AUDIO20_FORMAT_TYPE_I && + (class_info.format_bitmap & AUDIO20_DATA_FORMAT_TYPE_I_PCM) != 0; + break; + #endif + default: + break; + } + if (!pcm_supported) { + TU_LOG_DRV(" AUDIO AS itf %u: Type-I PCM format not supported\r\n", itf_num); + return p_desc; + } + tuh_audio_format_t format; + if (!audioh_format_from_pcm(class_info.subslot_size, class_info.bit_resolution, &format)) { + TU_LOG_DRV(" AUDIO AS itf %u: subslot %u bits %u not supported\r\n", itf_num, class_info.subslot_size, + class_info.bit_resolution); + return p_desc; + } + if (class_info.channels == 0) { + TU_LOG_DRV(" AUDIO AS itf %u: zero channels not supported\r\n", itf_num); + return p_desc; + } + + const uint16_t iso_xfer_size = + (tuh_speed_get(p_audio->daddr) == TUSB_SPEED_HIGH) ? TUSB_EPSIZE_ISO_HS_MAX : TUSB_EPSIZE_ISO_FS_MAX; + const uint32_t frame_bytes_32 = (uint32_t)class_info.channels * tuh_audio_format_bytes(format); + if (frame_bytes_32 == 0 || frame_bytes_32 > iso_xfer_size) { + TU_LOG_DRV(" AUDIO AS itf %u: frame size %lu not supported\r\n", itf_num, (unsigned long)frame_bytes_32); + return p_desc; + } + // Store the alternate setting once; the public API expands its sampling + // frequencies into separate configurations. + const audioh_ep_info_t *ep = &ep_info; + tuh_audio_stream_t *stream = audioh_get_stream(p_audio, tu_edpt_dir(ep->ep_addr)); + audioh_terminal_info_t terminal; + if (!audioh_ac_terminal_find(p_audio, ac_desc, class_info.terminal_id, &terminal) || + terminal.stream_dir != stream->dir) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: terminal %u does not match endpoint direction\r\n", itf_num, alt, + class_info.terminal_id); + return p_desc; + } + + const uint16_t epbuf_size = (stream->dir == TUSB_DIR_IN) ? CFG_TUH_AUDIO_EPIN_BUFSIZE : CFG_TUH_AUDIO_EPOUT_BUFSIZE; + + if (ep->ep_size == 0 || ep->ep_size > iso_xfer_size) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: invalid isochronous ep size %u\r\n", itf_num, alt, ep->ep_size); + return p_desc; + } + + // Capture always requests the endpoint's maximum packet size, so both the + // transfer buffer and FIFO must hold it. + if (stream->dir == TUSB_DIR_IN && (ep->ep_size > epbuf_size || ep->ep_size > CFG_TUH_AUDIO_STREAM_BUFSIZE)) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: capture ep size %u exceeds buffer capacity\r\n", itf_num, alt, ep->ep_size); + return p_desc; + } + + audioh_as_config_t as_config = {.ep_size = ep->ep_size, + .itf_num = itf_num, + .alt_setting = alt, + .ep_addr = ep->ep_addr, + .ep_interval = ep->ep_interval, + .ep_attr = ep->ep_attr, + .format = (uint8_t)format, + .channels = class_info.channels, + .terminal_id = class_info.terminal_id}; + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + audioh_rate_source_t rate_source = {0}; + #endif + + switch (p_audio->protocol) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + case AUDIO_INT_PROTOCOL_CODE_V1: + rate_source.control_id = ep->ep_addr; + rate_source.frequency_access = ep->sam_freq_ctrl ? AUDIOH_CTRL_READ_WRITE : AUDIOH_CTRL_NONE; + if (!audioh_uac1_rates_store(p_audio, stream, &as_config, &class_info, &rate_source)) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: no supported sampling frequency\r\n", itf_num, alt); + return p_desc; + } + break; + #endif + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + case AUDIO_INT_PROTOCOL_CODE_V2: { + const int8_t rate_source_idx = audioh_uac2_rate_source_get(p_audio, ac_desc, &terminal); + if (rate_source_idx < 0) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: direct Clock Source not found\r\n", itf_num, alt); + return p_desc; + } + as_config.rate_source_idx = (uint8_t)rate_source_idx; + break; + } + #endif + default: + return p_desc; + } + if (stream->as_count >= CFG_TUH_AUDIO_MAX_AS) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: reach max alternate settings %u\r\n", itf_num, alt, CFG_TUH_AUDIO_MAX_AS); + return p_desc; + } + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + if (p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V1 && p_audio->rate_source_count >= AUDIOH_MAX_RATE_SOURCES) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: reach max rate sources %u\r\n", itf_num, alt, AUDIOH_MAX_RATE_SOURCES); + return p_desc; + } + #endif + + const uint8_t as_idx = stream->as_count; + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + if (p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V1) { + as_config.rate_source_idx = p_audio->rate_source_count; + p_audio->rate_source[p_audio->rate_source_count++] = rate_source; + } + #endif + stream->as[as_idx] = as_config; + if (stream->dir == TUSB_DIR_OUT && has_feedback_ep) { + audioh_feedback_ep_t *feedback = &p_audio->playback.feedback[as_idx]; + feedback->ep_addr = fb_info.ep_addr; + feedback->ep_size = (uint8_t)fb_info.ep_size; + feedback->ep_interval = fb_info.ep_interval; + feedback->ep_attr = fb_info.ep_attr; + } + stream->as_count++; + stream->config_count += as_config.rate_count; + + return p_desc; +} + +uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len) { + (void)rhport; + + const uint8_t *desc_start = (const uint8_t *)desc_itf; + const uint8_t *p_desc = desc_start; + const uint8_t *desc_end = desc_start + max_len; + TU_VERIFY(audioh_desc_valid(p_desc, desc_end, sizeof(tusb_desc_interface_t)), 0); + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_type(desc_itf) == TUSB_DESC_INTERFACE), 0); + TU_VERIFY(TUSB_CLASS_AUDIO == desc_itf->bInterfaceClass, 0); + TU_VERIFY(AUDIO_SUBCLASS_CONTROL == desc_itf->bInterfaceSubClass, 0); + TU_VERIFY(audioh_protocol_enabled(desc_itf->bInterfaceProtocol), 0); + + const uint8_t idx = find_new_audio_index(); + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + p_audio->daddr = dev_addr; + p_audio->ac_itf_num = desc_itf->bInterfaceNumber; + p_audio->protocol = desc_itf->bInterfaceProtocol; + p_audio->rate_source_count = 0; + tu_memclr(p_audio->rate_source, sizeof(p_audio->rate_source)); + tu_memclr(&p_audio->ctrl, sizeof(p_audio->ctrl)); + audioh_stream_reset(&p_audio->in_stream); + audioh_stream_reset(&p_audio->out_stream); + audioh_playback_reset(&p_audio->playback); + p_audio->in_stream.daddr = dev_addr; + p_audio->out_stream.daddr = dev_addr; + + TU_LOG_DRV("AUDIO opening AC Interface %u (addr = %u)\r\n", desc_itf->bInterfaceNumber, dev_addr); + + p_desc = tu_desc_next(p_desc); + audioh_ac_desc_range_t ac_desc = {.desc_start = p_desc}; + while (p_desc < desc_end) { + if (!audioh_desc_valid(p_desc, desc_end, 2)) { + goto open_failed; + } + if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { + break; + } + + if (tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE) { + if (!audioh_desc_valid(p_desc, desc_end, 3)) { + goto open_failed; + } + if (!audioh_ac_entity_valid(p_audio, p_desc)) { + goto open_failed; + } + } + p_desc = tu_desc_next(p_desc); + } + ac_desc.desc_end = p_desc; + + // Audio Streaming interfaces belonging to this function immediately follow + // its Audio Control descriptor block. + while (p_desc < desc_end) { + if (!audioh_desc_valid(p_desc, desc_end, 2)) { + goto open_failed; + } + if (tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { + p_desc = tu_desc_next(p_desc); + continue; + } + + if (!audioh_desc_valid(p_desc, desc_end, sizeof(tusb_desc_interface_t))) { + goto open_failed; + } + const tusb_desc_interface_t *desc_interface = (const tusb_desc_interface_t *)p_desc; + if (desc_interface->bInterfaceClass != TUSB_CLASS_AUDIO || + desc_interface->bInterfaceSubClass != AUDIO_SUBCLASS_STREAMING || + desc_interface->bInterfaceProtocol != p_audio->protocol) { + break; + } + + TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", desc_interface->bInterfaceNumber, + desc_interface->bAlternateSetting); + p_desc = audioh_parse_as(p_audio, &ac_desc, desc_interface, p_desc, desc_end); + if (p_desc == NULL) { + goto open_failed; + } + } + + audioh_link_feature_units(p_audio, &ac_desc); + + if (p_audio->in_stream.as_count == 0 && p_audio->out_stream.as_count == 0) { + goto open_failed; + } + + // Assign contiguous public indices in playback-then-capture order. + uint8_t stream_idx = 0; + if (p_audio->out_stream.as_count > 0) { + p_audio->out_stream.stream_idx = stream_idx++; + } + if (p_audio->in_stream.as_count > 0) { + p_audio->in_stream.stream_idx = stream_idx++; + } + p_audio->stream_count = stream_idx; + + const tuh_audio_descriptor_cb_t desc_cb_data = { + .desc_audio_control = desc_itf, + .desc_cs_audio_control = ac_desc.desc_start, + .desc_cs_audio_control_len = (uint16_t)(ac_desc.desc_end - ac_desc.desc_start), + }; + tuh_audio_descriptor_cb(idx, &desc_cb_data); + + return (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_start); + +open_failed: + audioh_stream_reset(&p_audio->in_stream); + audioh_stream_reset(&p_audio->out_stream); + audioh_playback_reset(&p_audio->playback); + p_audio->daddr = 0; + p_audio->ac_itf_num = 0; + p_audio->protocol = 0; + p_audio->stream_count = 0; + p_audio->rate_source_count = 0; + p_audio->mounted = false; + return 0; +} + +//--------------------------------------------------------------------+ +// SET CONFIGURATION +//--------------------------------------------------------------------+ +static void audioh_mount_feature_unit_next(uint8_t idx); + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 +static void audioh_mount_clock_complete(tuh_xfer_t *xfer); + +static void audioh_uac2_configs_rebuild(audioh_interface_t *p_audio) { + p_audio->in_stream.config_count = 0; + p_audio->out_stream.config_count = 0; + + for (uint8_t direction = TUSB_DIR_OUT; direction <= TUSB_DIR_IN; direction++) { + tuh_audio_stream_t *stream = audioh_get_stream(p_audio, (tusb_dir_t)direction); + for (uint8_t as_idx = 0; as_idx < stream->as_count; as_idx++) { + audioh_as_config_t *as = &stream->as[as_idx]; + audioh_rate_source_t *rate_source = audioh_as_rate_source(stream, as); + as->rate_count = 0; + for (uint8_t rate_idx = 0; rate_idx < rate_source->sample_rate_count; rate_idx++) { + if (audioh_as_rate_fits(p_audio, stream, as, rate_source->sample_rate[rate_idx])) { + as->rate_count++; + } + } + stream->config_count += as->rate_count; + } + } + + uint8_t stream_idx = 0; + p_audio->out_stream.stream_idx = TUSB_INDEX_INVALID_8; + p_audio->in_stream.stream_idx = TUSB_INDEX_INVALID_8; + if (p_audio->out_stream.config_count > 0) { + p_audio->out_stream.stream_idx = stream_idx++; + } + if (p_audio->in_stream.config_count > 0) { + p_audio->in_stream.stream_idx = stream_idx++; + } + p_audio->stream_count = stream_idx; +} + +static bool audioh_uac2_clock_range_store(audioh_rate_source_t *rate_source, const uint8_t *buffer, uint16_t length) { + TU_VERIFY(length >= 2, false); + const uint16_t subrange_count = tu_le16toh(tu_unaligned_read16(buffer)); + const uint16_t available = (uint16_t)((length - 2u) / 12u); + TU_VERIFY(subrange_count > 0 && available > 0, false); + + rate_source->sample_rate_count = 0; + const uint16_t parsed_count = TU_MIN(subrange_count, available); + for (uint16_t i = 0; i < parsed_count && rate_source->sample_rate_count < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { + const uint8_t *subrange = &buffer[2u + 12u * i]; + const uint32_t min = tu_le32toh(tu_unaligned_read32(&subrange[0])); + const uint32_t max = tu_le32toh(tu_unaligned_read32(&subrange[4])); + const uint32_t res = tu_le32toh(tu_unaligned_read32(&subrange[8])); + if (min == 0 || min > max || (min != max && res == 0)) { + continue; + } + if (min == max) { + rate_source->sample_rate[rate_source->sample_rate_count++] = min; + continue; + } + for (uint32_t rate = min; rate <= max && rate_source->sample_rate_count < CFG_TUH_AUDIO_MAX_SAM_FREQ;) { + rate_source->sample_rate[rate_source->sample_rate_count++] = rate; + if (max - rate < res) { + break; + } + rate += res; + } + } + return rate_source->sample_rate_count > 0; +} + +static bool audioh_mount_clock_submit(uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + audioh_ctrl_state_t *ctrl = &p_audio->ctrl; + audioh_rate_source_t *rate_source = &p_audio->rate_source[ctrl->clock.rate_source_idx]; + const uint16_t length = ctrl->clock.read_cur ? 4u : (uint16_t)sizeof(epbuf->control.clock_range); + + const tusb_control_request_t request = { + .bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN}, + .bRequest = ctrl->clock.read_cur ? AUDIO20_CS_REQ_CUR : AUDIO20_CS_REQ_RANGE, + .wValue = tu_htole16(tu_u16(AUDIO20_CS_CTRL_SAM_FREQ, 0)), + .wIndex = tu_htole16(tu_u16(rate_source->control_id, p_audio->ac_itf_num)), + .wLength = tu_htole16(length), + }; + tuh_xfer_t xfer = {.daddr = p_audio->daddr, + .ep_addr = 0, + .setup = &request, + .buffer = epbuf->control.clock_range, + .complete_cb = audioh_mount_clock_complete, + .user_data = (uintptr_t)idx}; + return tuh_control_xfer(&xfer); +} + +static void audioh_mount_clock_finish(uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + audioh_ctrl_state_t *ctrl = &p_audio->ctrl; + ctrl->fu_busy = false; + audioh_uac2_configs_rebuild(p_audio); + + if (p_audio->stream_count == 0) { + const uint8_t daddr = p_audio->daddr; + const uint8_t itf_num = p_audio->ac_itf_num; + audioh_stream_reset(&p_audio->in_stream); + audioh_stream_reset(&p_audio->out_stream); + audioh_playback_reset(&p_audio->playback); + p_audio->daddr = 0; + p_audio->ac_itf_num = 0; + p_audio->protocol = 0; + p_audio->rate_source_count = 0; + usbh_driver_set_config_complete(daddr, itf_num); + return; + } + + ctrl->fu.mount.stream_idx = 0; + audioh_mount_feature_unit_next(idx); +} + +static void audioh_mount_clock_next(uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + audioh_ctrl_state_t *ctrl = &p_audio->ctrl; + + while (ctrl->clock.rate_source_idx < p_audio->rate_source_count) { + ctrl->clock.read_cur = false; + ctrl->fu_busy = true; + if (audioh_mount_clock_submit(idx)) { + return; + } + p_audio->rate_source[ctrl->clock.rate_source_idx].sample_rate_count = 0; + ctrl->clock.rate_source_idx++; + } + audioh_mount_clock_finish(idx); +} + +static void audioh_mount_clock_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; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + if (!ctrl->fu_busy) { + return; + } + audioh_rate_source_t *rate_source = &p_audio->rate_source[ctrl->clock.rate_source_idx]; + + bool success = xfer->result == XFER_RESULT_SUCCESS; + if (success && ctrl->clock.read_cur) { + success = xfer->actual_len == 4; + if (success) { + const uint32_t current = tu_le32toh(tu_unaligned_read32(epbuf->control.clock_range)); + success = current > 0; + if (success) { + rate_source->sample_rate[0] = current; + rate_source->sample_rate_count = 1; + } + } + } else if (success) { + success = audioh_uac2_clock_range_store(rate_source, epbuf->control.clock_range, (uint16_t)xfer->actual_len); + if (success && rate_source->frequency_access == AUDIOH_CTRL_READ) { + ctrl->clock.read_cur = true; + if (audioh_mount_clock_submit(idx)) { + return; + } + success = false; + } + } + + if (!success) { + rate_source->sample_rate_count = 0; + } + ctrl->fu_busy = false; + ctrl->clock.rate_source_idx++; + audioh_mount_clock_next(idx); +} + #endif + +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++) { + if (_audioh_itf[i].daddr == dev_addr && _audioh_itf[i].ac_itf_num == itf_num) { + idx = i; + break; + } + } + + if (idx == TUSB_INDEX_INVALID_8) { + // Only the Audio Control interface drives mounting. Streaming alternate + // settings are selected later by tuh_audio_start(). + usbh_driver_set_config_complete(dev_addr, itf_num); + return true; + } + + audioh_ctrl_state_t *ctrl = &_audioh_itf[idx].ctrl; + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + if (_audioh_itf[idx].protocol == AUDIO_INT_PROTOCOL_CODE_V2) { + ctrl->clock.rate_source_idx = 0; + audioh_mount_clock_next(idx); + } else + #endif + { + ctrl->fu.mount.stream_idx = 0; + audioh_mount_feature_unit_next(idx); + } + return true; +} + +//--------------------------------------------------------------------+ +// APPLICATION API +//--------------------------------------------------------------------+ +bool tuh_audio_mounted(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); + return _audioh_itf[idx].mounted; +} + +uint8_t tuh_audio_get_dev_addr(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); + return _audioh_itf[idx].daddr; +} + +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_access != AUDIOH_CTRL_NONE; +} + +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_range_channel != TUSB_INDEX_INVALID_8, 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]; + TU_VERIFY(p_audio->daddr != 0, 0); + return p_audio->stream_count; +} + +bool tuh_audio_stream_exists(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, false); + return audioh_get_stream_by_idx(p_audio, stream_idx) != NULL; +} + +tuh_audio_direction_t tuh_audio_stream_direction(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, TUH_AUDIO_STREAM_DIRECTION_COUNT); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, TUH_AUDIO_STREAM_DIRECTION_COUNT); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s, TUH_AUDIO_STREAM_DIRECTION_COUNT); + return (s->dir == TUSB_DIR_IN) ? TUH_AUDIO_STREAM_CAPTURE : TUH_AUDIO_STREAM_PLAYBACK; +} + +uint8_t tuh_audio_config_count(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, 0); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s, 0); + return s->config_count; +} +uint8_t tuh_audio_active_config(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, TUSB_INDEX_INVALID_8); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, TUSB_INDEX_INVALID_8); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s, TUSB_INDEX_INVALID_8); + return s->active_config; +} +bool tuh_audio_config_get(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx, tuh_audio_stream_config_t *config) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, false); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && config, false); + + return audioh_stream_config_get(s, config_idx, config); +} + +bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->mounted, false); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s, false); + tuh_audio_stream_config_t cfg; + uint8_t as_idx; + uint8_t rate_idx; + TU_VERIFY(audioh_stream_resolve_config(s, config_idx, &as_idx, &rate_idx), false); + audioh_stream_config_fill(s, as_idx, rate_idx, &cfg); + TU_VERIFY(!s->running, false); + if (s->state == STREAM_STATE_READY) { + // Configuration cannot close an endpoint while its final transfer drains. + TU_VERIFY(!usbh_edpt_busy(s->daddr, s->edpt.ep_addr), false); + if (s->dir == TUSB_DIR_OUT && p_audio->playback.feedback_opened) { + TU_VERIFY(!usbh_edpt_busy(s->daddr, p_audio->playback.feedback[s->active_as].ep_addr), false); + } + } + + // Reopen even when the address is unchanged: packet size and interval belong + // to the alternate setting and may differ. + TU_VERIFY(audioh_stream_close_ep(s), false); + + const audioh_as_config_t *as = &s->as[as_idx]; + s->active_config = config_idx; + s->active_as = as_idx; + s->active_rate = rate_idx; + s->frame_bytes = (uint16_t)tuh_audio_config_frame_size(&cfg); + s->state = STREAM_STATE_IDLE; + if (s->dir == TUSB_DIR_OUT) { + const uint32_t frame_div = (tuh_speed_get(s->daddr) == TUSB_SPEED_HIGH) ? 8000u : 1000u; + audioh_playback_t *playback = &p_audio->playback; + playback->nominal_frames_q16 = audioh_nominal_frames_q16(cfg.sample_rate, as->ep_interval, s->daddr); + playback->target_frames_q16 = playback->nominal_frames_q16; + playback->feedback_min_frames = (uint16_t)((cfg.sample_rate - 1u) / frame_div); + playback->feedback_max_frames = (uint16_t)(cfg.sample_rate / frame_div + 1u); + playback->rem_acc = 0; + } + if (s->dir == TUSB_DIR_IN) { + // Overwrite mode is frame-safe only when FIFO depth is a whole-frame multiple. + const uint16_t fifo_depth = CFG_TUH_AUDIO_STREAM_BUFSIZE - (CFG_TUH_AUDIO_STREAM_BUFSIZE % s->frame_bytes); + if (!tu_fifo_config(&s->edpt.ff, s->ff_buf, fifo_depth, true)) { + audioh_stream_fail(s); + return false; + } + } + + TU_LOG_DRV(" AUDIO configure %s stream %u: itf %u alt %u ep %02x\r\n", + (s->dir == TUSB_DIR_IN) ? "capture" : "playback", s->stream_idx, as->itf_num, as->alt_setting, + as->ep_addr); + + return audioh_stream_open_ep(s); +} + +// Start endpoint transfers after the alternate setting and sampling frequency +// are both active. +static bool audioh_stream_start_xfer(tuh_audio_stream_t *s) { + if (s->dir == TUSB_DIR_IN) { + return audioh_stream_capture_xfer(s); + } else { + if (audioh_get_playback(s)->feedback[s->active_as].ep_addr != 0) { + TU_VERIFY(audioh_stream_feedback_xfer(s), false); + } + return audioh_stream_playback_xfer(s); + } +} + +static void audioh_stream_start_done(tuh_audio_stream_t *s, tusb_xfer_result_t result) { + if (result != XFER_RESULT_SUCCESS) { + audioh_stream_stop_xfers(s); + } + s->operation = AUDIOH_STREAM_OP_NONE; + tuh_audio_event_cb(s->idx, s->stream_idx, TUH_AUDIO_EVENT_START_COMPLETE, result); +} + +static void audioh_stream_start_xfers(tuh_audio_stream_t *s) { + const tusb_xfer_result_t result = audioh_stream_start_xfer(s) ? XFER_RESULT_SUCCESS : XFER_RESULT_FAILED; + audioh_stream_start_done(s, result); +} + +static void audioh_stream_start_complete(tuh_xfer_t *xfer); + +static bool audioh_stream_activate(tuh_audio_stream_t *s) { + const audioh_as_config_t *as = audioh_stream_active_as(s); + return tuh_interface_set(s->daddr, as->itf_num, as->alt_setting, audioh_stream_start_complete, (uintptr_t)s); +} + +static void audioh_stream_start_set_freq_complete(tuh_xfer_t *xfer) { + tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; + if (s->daddr != xfer->daddr || s->state != STREAM_STATE_READY || !s->running) { + // Ignore a completion delivered after disconnect or stop. + return; + } + if (xfer->result != XFER_RESULT_SUCCESS) { + TU_LOG_DRV(" AUDIO set sampling frequency failed: result=%u\r\n", xfer->result); + audioh_stream_start_done(s, xfer->result); + return; + } + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + if (_audioh_itf[s->idx].protocol == AUDIO_INT_PROTOCOL_CODE_V2) { + if (!audioh_stream_activate(s)) { + audioh_stream_start_done(s, XFER_RESULT_FAILED); + } + } else + #endif + { + audioh_stream_start_xfers(s); + } +} + +static void audioh_stream_start_active(tuh_audio_stream_t *s) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + const audioh_as_config_t *as = audioh_stream_active_as(s); + const audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); + if (_audioh_itf[s->idx].protocol == AUDIO_INT_PROTOCOL_CODE_V1 && + rate_source->frequency_access == AUDIOH_CTRL_READ_WRITE) { + if (!audioh_stream_set_freq(s, audioh_stream_start_set_freq_complete)) { + audioh_stream_start_done(s, XFER_RESULT_FAILED); + } + return; + } else + #endif + { + audioh_stream_start_xfers(s); + } +} + +static void audioh_stream_start_complete(tuh_xfer_t *xfer) { + tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; + if (s->daddr != xfer->daddr || s->state != STREAM_STATE_READY || !s->running) { + // Ignore a completion delivered after disconnect or stop. + return; + } + if (xfer->result != XFER_RESULT_SUCCESS) { + TU_LOG_DRV(" AUDIO SET_INTERFACE activate failed: result=%u\r\n", xfer->result); + audioh_stream_start_done(s, xfer->result); + return; + } + audioh_stream_start_active(s); +} + +bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->mounted, false); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s, false); + TU_VERIFY(s->state == STREAM_STATE_READY && s->operation == AUDIOH_STREAM_OP_NONE && !s->running, false); + // A stopped transfer must drain before the endpoint can be restarted. + const audioh_as_config_t *as = audioh_stream_active_as(s); + const audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); + TU_VERIFY(!usbh_edpt_busy(s->daddr, as->ep_addr), false); + if (s->dir == TUSB_DIR_OUT && p_audio->playback.feedback_opened) { + TU_VERIFY(!usbh_edpt_busy(s->daddr, p_audio->playback.feedback[s->active_as].ep_addr), false); + } + + // Capture and playback must use the same rate while both are running. + tuh_audio_stream_t *other = (s == &p_audio->out_stream) ? &p_audio->in_stream : &p_audio->out_stream; + if (other->running) { + const audioh_as_config_t *other_as = audioh_stream_active_as(other); + const audioh_rate_source_t *other_rate_source = audioh_as_rate_source(other, other_as); + const uint32_t sample_rate = rate_source->sample_rate[s->active_rate]; + const uint32_t other_sample_rate = other_rate_source->sample_rate[other->active_rate]; + if (sample_rate != other_sample_rate) { + TU_LOG_DRV(" AUDIO start failed: capture/playback sample rates must match (%lu != %lu)\r\n", + (unsigned long)sample_rate, (unsigned long)other_sample_rate); + return false; + } + } + + if (s->dir == TUSB_DIR_OUT) { + p_audio->playback.target_frames_q16 = p_audio->playback.nominal_frames_q16; + p_audio->playback.rem_acc = 0; + } + s->running = true; + s->operation = AUDIOH_STREAM_OP_START; + // UAC2 controls a Clock Source that exists before endpoint activation. UAC1 + // controls the endpoint itself, so its alternate setting must be active first. + bool submitted = false; + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + if (p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2 && rate_source->frequency_access == AUDIOH_CTRL_READ_WRITE) { + submitted = audioh_stream_set_freq(s, audioh_stream_start_set_freq_complete); + } else + #endif + { + submitted = audioh_stream_activate(s); + } + if (!submitted) { + s->operation = AUDIOH_STREAM_OP_NONE; + s->running = false; + return false; + } + return true; +} + +static void audioh_stream_stop_complete(tuh_xfer_t *xfer) { + tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; + if (s->daddr != xfer->daddr || s->operation != AUDIOH_STREAM_OP_STOP) { + return; + } + TU_LOG_DRV(" AUDIO SET_INTERFACE deactivate done: result=%u\r\n", xfer->result); + s->operation = AUDIOH_STREAM_OP_NONE; + tuh_audio_event_cb(s->idx, s->stream_idx, TUH_AUDIO_EVENT_STOP_COMPLETE, xfer->result); +} + +bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[dev_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->state == STREAM_STATE_READY && s->operation == AUDIOH_STREAM_OP_NONE && s->running, false); + + const audioh_as_config_t *as = audioh_stream_active_as(s); + // Preserve running state when submission fails so the caller can retry. + TU_VERIFY(tuh_interface_set(s->daddr, as->itf_num, 0, audioh_stream_stop_complete, (uintptr_t)s), false); + + // SET_INTERFACE stops future traffic. The current transfer drains, while its + // data and all queued frames are discarded. + s->operation = AUDIOH_STREAM_OP_STOP; + audioh_stream_stop_xfers(s); + return true; +} + +uint32_t tuh_audio_write(uint8_t dev_idx, uint8_t stream_idx, const void *buffer, uint32_t frame_count) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->mounted && buffer, 0); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && s->dir == TUSB_DIR_OUT, 0); + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0); + + // Never split an audio frame at the FIFO boundary. + const uint32_t frames = TU_MIN(frame_count, tu_fifo_remaining(&s->edpt.ff) / s->frame_bytes); + if (frames == 0) { + return 0; + } + tu_fifo_write_n(&s->edpt.ff, buffer, (uint16_t)(frames * s->frame_bytes)); + + return frames; +} + +uint32_t tuh_audio_read(uint8_t dev_idx, uint8_t stream_idx, void *buffer, uint32_t frame_count) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->mounted && buffer, 0); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && s->dir == TUSB_DIR_IN, 0); + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0); + + // Never return a partial audio frame. + const uint32_t frames = TU_MIN(frame_count, tu_fifo_count(&s->edpt.ff) / s->frame_bytes); + if (frames > 0) { + tu_fifo_read_n(&s->edpt.ff, buffer, (uint16_t)(frames * s->frame_bytes)); + } + return frames; +} + +uint32_t tuh_audio_write_available(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, 0); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && s->dir == TUSB_DIR_OUT, 0); + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0); + return tu_edpt_stream_write_available(&s->edpt) / s->frame_bytes; +} + +uint32_t tuh_audio_read_available(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, 0); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && s->dir == TUSB_DIR_IN, 0); + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0); + return tu_edpt_stream_read_available(&s->edpt) / s->frame_bytes; +} + +//--------------------------------------------------------------------+ +// AUDIO CONTROL REQUESTS +//--------------------------------------------------------------------+ + +static void audioh_fu_set_complete(tuh_xfer_t *xfer); + +enum { + AUDIOH_FU_VALUE_BOOL, + AUDIOH_FU_VALUE_I16 +}; + +static uint8_t audioh_control_cur_request(uint8_t protocol, tusb_dir_t direction) { + #if !(CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1) + (void)direction; + #endif + switch (protocol) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + case AUDIO_INT_PROTOCOL_CODE_V1: + return (direction == TUSB_DIR_IN) ? AUDIO10_CS_REQ_GET_CUR : AUDIO10_CS_REQ_SET_CUR; + #endif + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + case AUDIO_INT_PROTOCOL_CODE_V2: + return AUDIO20_CS_REQ_CUR; + #endif + default: + return 0; + } +} + +static bool audioh_control_submit(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, + tuh_xfer_t *xfer) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX && entity_id != 0 && request != 0, false); + TU_VERIFY(direction == TUSB_DIR_OUT || direction == TUSB_DIR_IN, false); + TU_VERIFY(buffer != NULL || length == 0, false); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio->mounted, false); + + const tusb_control_request_t setup = { + .bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = direction}, + .bRequest = request, + .wValue = tu_htole16(tu_u16(control_selector, channel)), + .wIndex = tu_htole16(tu_u16(entity_id, p_audio->ac_itf_num)), + .wLength = tu_htole16(length), + }; + xfer->daddr = p_audio->daddr; + xfer->ep_addr = 0; + xfer->setup = &setup; + xfer->buffer = buffer; + return tuh_control_xfer(xfer); +} + +bool tuh_audio_control_xfer(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, + tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + tuh_xfer_t xfer = {.complete_cb = complete_cb, .user_data = user_data}; + return audioh_control_submit(idx, entity_id, direction, request, control_selector, channel, buffer, length, &xfer); +} + +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, + uint32_t *actual_len) { + if (actual_len != NULL) { + *actual_len = 0; + } + + tuh_xfer_t xfer = {0}; + if (!audioh_control_submit(idx, entity_id, direction, request, control_selector, channel, buffer, length, &xfer)) { + return XFER_RESULT_TIMEOUT; + } + + if (actual_len != NULL) { + *actual_len = xfer.actual_len; + } + 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; + } else { + const uint16_t value = tu_le16toh(tu_unaligned_read16(audioh_fu_ctrl(epbuf))); + *((int16_t *)ctrl->value) = (int16_t)value; + } +} + +// Convert the driver-owned response before releasing the request state and +// invoking the application callback. +static void audioh_fu_get_complete(tuh_xfer_t *xfer) { + const uint8_t idx = (uint8_t)xfer->user_data; + audioh_ctrl_state_t *ctrl = &_audioh_itf[idx].ctrl; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + tuh_xfer_cb_t app_cb = ctrl->complete_cb; + uintptr_t user_data = ctrl->user_data; + ctrl->complete_cb = NULL; + ctrl->fu_busy = false; + + if (ctrl->value != NULL && xfer->result == XFER_RESULT_SUCCESS) { + if (xfer->actual_len == ctrl->fu.control.width) { + audioh_fu_value_store(ctrl, epbuf); + } else { + xfer->result = XFER_RESULT_FAILED; + } + } + + xfer->user_data = user_data; + if (app_cb != NULL) { + app_cb(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(tuh_audio_stream_t *s, audioh_ctrl_state_t *ctrl, audioh_epbuf_t *epbuf) { + const uint16_t value = tu_le16toh(tu_unaligned_read16(audioh_fu_ctrl(epbuf))); + switch (ctrl->fu.mount.range_step) { + case AUDIOH_VOLUME_RANGE_MIN: + s->volume_range.min = (int16_t)value; + break; + case AUDIOH_VOLUME_RANGE_MAX: + s->volume_range.max = (int16_t)value; + break; + case AUDIOH_VOLUME_RANGE_RES: + s->volume_range.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_ctrl_state_t *ctrl = &p_audio->ctrl; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + tuh_audio_stream_t *s = audioh_get_stream_by_idx_unchecked(p_audio, ctrl->fu.mount.stream_idx); + + const bool uac2 = p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2; + const uint8_t selector = uac2 ? AUDIO20_FU_CTRL_VOLUME : AUDIO10_FU_CTRL_VOLUME; + 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, s->volume_range_channel)), + .wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)), + .wLength = tu_htole16(uac2 ? 8u : 2u), + }; + tuh_xfer_t xfer = {.daddr = p_audio->daddr, + .ep_addr = 0, + .setup = &request, + .buffer = audioh_fu_ctrl(epbuf), + .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_ctrl_state_t *ctrl = &p_audio->ctrl; + + 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_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_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; + } + ctrl->fu_busy = false; + } + ctrl->fu.mount.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_interface_t *p_audio = &_audioh_itf[idx]; + audioh_ctrl_state_t *ctrl = &p_audio->ctrl; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + if (!ctrl->fu_busy) { + return; + } + tuh_audio_stream_t *s = audioh_get_stream_by_idx_unchecked(p_audio, ctrl->fu.mount.stream_idx); + + const bool uac2 = p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2; + if (uac2 && xfer->result == XFER_RESULT_SUCCESS && xfer->actual_len == 8 && + tu_le16toh(tu_unaligned_read16(audioh_fu_ctrl(epbuf))) == 1) { + uint8_t *fu_ctrl = audioh_fu_ctrl(epbuf); + s->volume_range.min = (int16_t)tu_le16toh(tu_unaligned_read16(&fu_ctrl[2])); + s->volume_range.max = (int16_t)tu_le16toh(tu_unaligned_read16(&fu_ctrl[4])); + s->volume_range.res = tu_le16toh(tu_unaligned_read16(&fu_ctrl[6])); + if (s->volume_range.min > s->volume_range.max || s->volume_range.res == 0) { + xfer->result = XFER_RESULT_FAILED; + } + } else if (!uac2 && xfer->result == XFER_RESULT_SUCCESS && xfer->actual_len == 2) { + audioh_fu_volume_range_store(s, ctrl, epbuf); + ctrl->fu.mount.range_step++; + + if (ctrl->fu.mount.range_step < AUDIOH_VOLUME_RANGE_COUNT) { + if (audioh_mount_feature_unit_submit(idx)) { + return; + } + xfer->result = XFER_RESULT_FAILED; + } else if (s->volume_range.min > s->volume_range.max || s->volume_range.res == 0) { + xfer->result = XFER_RESULT_FAILED; + } + } + + const uint32_t expected_len = uac2 ? 8u : 2u; + if (xfer->result != XFER_RESULT_SUCCESS || xfer->actual_len != expected_len) { + 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; + } + } + ctrl->fu_busy = false; + ctrl->fu.mount.stream_idx++; + audioh_mount_feature_unit_next(idx); +} + +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(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_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); + audioh_ctrl_state_t *ctrl = &p_audio->ctrl; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + TU_VERIFY(!ctrl->fu_busy, false); + // Reserve both bookkeeping and payload storage before populating the request. + ctrl->fu_busy = true; + + uint8_t *val_buf = audioh_fu_ctrl(epbuf); + val_buf[0] = (uint8_t)(value & 0xFF); + if (width == 2) { + val_buf[1] = (uint8_t)((value >> 8) & 0xFF); + } + + if (complete_cb == NULL) { + 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; + 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, 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, 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(channel == 0 && s->mute_access != AUDIOH_CTRL_NONE, false); + } else if (control_selector == AUDIO10_FU_CTRL_VOLUME) { + 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); + audioh_ctrl_state_t *ctrl = &p_audio->ctrl; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + TU_VERIFY(!ctrl->fu_busy, false); + ctrl->fu_busy = true; + ctrl->value = value; + ctrl->fu.control.width = width; + ctrl->fu.control.value_type = value_type; + + if (complete_cb == NULL) { + // 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, channel, + audioh_fu_ctrl(epbuf), width, &xfer)) { + ctrl->fu_busy = false; + return false; + } + if (xfer.result == XFER_RESULT_SUCCESS && xfer.actual_len == width) { + audioh_fu_value_store(ctrl, epbuf); + } else if (xfer.result == XFER_RESULT_SUCCESS && user_data != 0) { + *((tusb_xfer_result_t *)user_data) = XFER_RESULT_FAILED; + } + ctrl->fu_busy = false; + return true; + } + + // The asynchronous wrapper converts the response before calling the application. + ctrl->complete_cb = complete_cb; + 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, channel, + audioh_fu_ctrl(epbuf), width, &xfer)) { + ctrl->complete_cb = NULL; + ctrl->fu_busy = false; + return false; + } + return true; +} + +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, 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, 0, mute, 1, AUDIOH_FU_VALUE_BOOL, complete_cb, 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); + 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; + } + 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); + } + 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, 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, 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 new file mode 100644 index 000000000..95911ab3b --- /dev/null +++ b/src/class/audio/audio_host.h @@ -0,0 +1,361 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Zhenjiang Zhang + * SPDX-FileCopyrightText: Copyright (c) 2026 HiFiPhile (Zixun LI) + * SPDX-License-Identifier: MIT + * + * This file is part of the TinyUSB stack. + */ + +#ifndef TUSB_AUDIO_HOST_H_ +#define TUSB_AUDIO_HOST_H_ + +#include "audio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Class Driver Configuration +//--------------------------------------------------------------------+ + +// Audio Class protocol versions compiled into the host driver. Multiple +// versions can be enabled so UAC1 and UAC2 devices can be mounted together. +#define TUH_AUDIO_PROTOCOL_UAC1 TU_BIT(0) +#define TUH_AUDIO_PROTOCOL_UAC2 TU_BIT(1) + +#ifndef CFG_TUH_AUDIO_PROTOCOLS + #define CFG_TUH_AUDIO_PROTOCOLS TUH_AUDIO_PROTOCOL_UAC1 +#endif + +#if !(CFG_TUH_AUDIO_PROTOCOLS & (TUH_AUDIO_PROTOCOL_UAC1 | TUH_AUDIO_PROTOCOL_UAC2)) + #error CFG_TUH_AUDIO_PROTOCOLS must enable UAC1 and/or UAC2 +#endif + +#if CFG_TUH_AUDIO_PROTOCOLS & ~(TUH_AUDIO_PROTOCOL_UAC1 | TUH_AUDIO_PROTOCOL_UAC2) + #error CFG_TUH_AUDIO_PROTOCOLS contains an unsupported protocol bit +#endif + +// Maximum number of Audio devices +#ifndef CFG_TUH_AUDIO_MAX + #define CFG_TUH_AUDIO_MAX 1 +#endif +// Maximum discrete sampling frequencies retained per rate source. UAC1 uses +// one rate source per alternate setting; UAC2 alternate settings may share a +// Clock Source. +#ifndef CFG_TUH_AUDIO_MAX_SAM_FREQ + #define CFG_TUH_AUDIO_MAX_SAM_FREQ 5 +#endif +// Maximum supported nonzero-bandwidth Audio Streaming alternate settings per +// logical stream. +#ifndef CFG_TUH_AUDIO_MAX_AS + #define CFG_TUH_AUDIO_MAX_AS 4 +#endif + +// Maximum size of one capture (IN) isochronous transfer the driver submits. +// Configurations needing a larger per-poll-interval packet are rejected. +// 256 covers 2-ch 48 kHz S16_LE (192 B) and common endpoint padding (208 B). +#ifndef CFG_TUH_AUDIO_EPIN_BUFSIZE + #define CFG_TUH_AUDIO_EPIN_BUFSIZE 256 +#endif + +// Maximum size of one playback (OUT) isochronous transfer the driver submits. +// Configurations needing a larger per-poll-interval packet are rejected. +#ifndef CFG_TUH_AUDIO_EPOUT_BUFSIZE + #define CFG_TUH_AUDIO_EPOUT_BUFSIZE 256 +#endif + +// Depth in bytes of the per-stream data FIFO. The FIFO decouples the +// application's read/write calls from the endpoint's isochronous polling cadence +// and absorbs rate differences. Capture overwrites the oldest frames when full. +// 1024 bytes hold 4 default (256 B) packets. +#ifndef CFG_TUH_AUDIO_STREAM_BUFSIZE + #define CFG_TUH_AUDIO_STREAM_BUFSIZE 1024 +#endif + +//--------------------------------------------------------------------+ +// Types +//--------------------------------------------------------------------+ + +// Fixed transfer direction of a logical stream. +typedef enum { + TUH_AUDIO_STREAM_PLAYBACK = 0, // Host -> Device (OUT) + TUH_AUDIO_STREAM_CAPTURE = 1, // Device -> Host (IN) + TUH_AUDIO_STREAM_DIRECTION_COUNT +} tuh_audio_direction_t; + +// Asynchronous stream operation and transport events. +typedef enum { + TUH_AUDIO_EVENT_START_COMPLETE = 0, + TUH_AUDIO_EVENT_STOP_COMPLETE, + TUH_AUDIO_EVENT_XFER_FAILED +} tuh_audio_event_t; + +// Discrete Type-I PCM sample format. UAC1 requires bSamFreqType > 0; UAC2 +// configurations are built from the directly connected Clock Source RANGE. +typedef enum { + TUH_AUDIO_FORMAT_S8 = 0, // signed 8-bit + TUH_AUDIO_FORMAT_S16_LE, // signed 16-bit little-endian + TUH_AUDIO_FORMAT_S24_3LE, // signed 24-bit packed in 3 bytes, LE + TUH_AUDIO_FORMAT_S24_LE, // signed 24-bit in 32-bit container, LE + TUH_AUDIO_FORMAT_S32_LE, // signed 32-bit little-endian + TUH_AUDIO_FORMAT_COUNT +} tuh_audio_format_t; + +// One complete supported discrete configuration tuple. +// Each entry is a full (format, sample_rate, channels) combination, +// avoiding invalid mixes between independent format/rate/channel lists. +// dir is constant for all configs of a given (dev_idx, stream_idx) and +// equals the result of tuh_audio_stream_direction(). +typedef struct { + uint32_t sample_rate; + tuh_audio_direction_t dir; + tuh_audio_format_t format; + uint8_t channels; +} tuh_audio_stream_config_t; + +// Feature Unit channel zero selects the master channel. +#define TUH_AUDIO_CHANNEL_MASTER 0 + +// 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; + +// Audio Control descriptors reported during enumeration. Descriptor pointers +// are valid only for the duration of tuh_audio_descriptor_cb(). +typedef struct { + const tusb_desc_interface_t *desc_audio_control; + const uint8_t *desc_cs_audio_control; + uint16_t desc_cs_audio_control_len; +} tuh_audio_descriptor_cb_t; + +//--------------------------------------------------------------------+ +// Stream Enumeration +//--------------------------------------------------------------------+ + +// Number of logical audio streams exposed by one mounted device. The +// application iterates stream indices [0, tuh_audio_stream_count()) and +// inspects each with tuh_audio_stream_exists()/tuh_audio_stream_direction(). +uint8_t tuh_audio_stream_count(uint8_t dev_idx); + +// True if (dev_idx, stream_idx) identifies an existing stream. +bool tuh_audio_stream_exists(uint8_t dev_idx, uint8_t stream_idx); + +// Fixed transfer direction of the stream. +tuh_audio_direction_t tuh_audio_stream_direction(uint8_t dev_idx, uint8_t stream_idx); + +//--------------------------------------------------------------------+ +// Configuration Enumeration +//--------------------------------------------------------------------+ + +// Number of supported discrete configurations of the stream. +uint8_t tuh_audio_config_count(uint8_t dev_idx, uint8_t stream_idx); + +// Active configuration index of the stream, or TUSB_INDEX_INVALID_8 if none. +uint8_t tuh_audio_active_config(uint8_t dev_idx, uint8_t stream_idx); + +// Retrieve one discrete configuration tuple into *config. +bool tuh_audio_config_get(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx, tuh_audio_stream_config_t *config); + +//--------------------------------------------------------------------+ +// Configuration (ALSA hw_params analogue) +//--------------------------------------------------------------------+ + +// Synchronously configure the stream with the discrete configuration identified by +// config_idx. The driver: +// 1. resolves the AS interface and alternate setting, +// 2. initializes the FIFO and packet scheduler, +// 3. opens / reconfigures only the selected endpoint. +bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx); + +//--------------------------------------------------------------------+ +// Stream Control / Frame-based Data +//--------------------------------------------------------------------+ + +// Start transferring data with the configuration selected by configure(). +// UAC1 activates the alternate setting before setting an endpoint frequency; +// UAC2 sets a writable Clock Source before activating the alternate setting. +// Startup is asynchronous: true means that the first request was submitted. +// Completion is reported through tuh_audio_event_cb(); no event is emitted +// when this function returns false. +bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx); +// Stop transferring and asynchronously deactivate the Audio Streaming +// interface (alt 0). true means that the deactivation request was submitted. +// Completion is reported through tuh_audio_event_cb(); no event is emitted +// when this function returns false. +bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx); + +// Frame-based transfer. One frame = channels * bytes per sample. +// tuh_audio_write() is valid only for TUH_AUDIO_STREAM_PLAYBACK streams, +// tuh_audio_read() only for TUH_AUDIO_STREAM_CAPTURE streams. +// Both functions are non-blocking and return immediately. +// Returns the number of frames actually written/read (0 on any error, +// including wrong direction, unconfigured/stopped stream, or full/empty FIFO). +uint32_t tuh_audio_write(uint8_t dev_idx, uint8_t stream_idx, const void *buffer, uint32_t frame_count); +uint32_t tuh_audio_read(uint8_t dev_idx, uint8_t stream_idx, void *buffer, uint32_t frame_count); + +// Number of frames that can be queued immediately for playback. +uint32_t tuh_audio_write_available(uint8_t dev_idx, uint8_t stream_idx); +// Number of captured frames that can be read immediately. +uint32_t tuh_audio_read_available(uint8_t dev_idx, uint8_t stream_idx); + +//--------------------------------------------------------------------+ +// Helpers +//--------------------------------------------------------------------+ + +// Container size in bytes of one sample for a given format. +static inline uint8_t tuh_audio_format_bytes(tuh_audio_format_t format) { + switch (format) { + case TUH_AUDIO_FORMAT_S8: + return 1; + case TUH_AUDIO_FORMAT_S16_LE: + return 2; + case TUH_AUDIO_FORMAT_S24_3LE: + return 3; + case TUH_AUDIO_FORMAT_S24_LE: + case TUH_AUDIO_FORMAT_S32_LE: + return 4; + default: + return 0; + } +} + +// Size in bytes of one frame (all channels) for a configuration. +static inline uint32_t tuh_audio_config_frame_size(const tuh_audio_stream_config_t *config) { + TU_ASSERT(config != NULL); + return (uint32_t)tuh_audio_format_bytes(config->format) * config->channels; +} + +//--------------------------------------------------------------------+ +// Device Info +//--------------------------------------------------------------------+ + +// Check if Audio device is mounted +bool tuh_audio_mounted(uint8_t idx); +// Get device address of Audio device +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 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); + +//--------------------------------------------------------------------+ +// Control Request API +//--------------------------------------------------------------------+ + +// Submit a class-specific request to an entity on the Audio Control interface. +// request is the protocol-specific UAC request code. buffer contains the raw +// little-endian control payload. For an asynchronous transfer, buffer must +// remain valid until complete_cb is invoked. +bool tuh_audio_control_xfer(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, + 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 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, 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, 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, + uint32_t *actual_len); + +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, + 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, + uint8_t channel, int16_t *volume) { + TU_API_SYNC(tuh_audio_volume_get, idx, stream_idx, channel, volume); +} + +//--------------------------------------------------------------------+ +// Callbacks (Weak is optional) +//--------------------------------------------------------------------+ + +// Invoked after the Audio Control and Streaming descriptors have been +// validated during enumeration, before tuh_audio_mount_cb(). The interface is +// not mounted yet and control requests must not be submitted from this +// callback. Applications may inspect or copy descriptors needed for later raw +// entity control requests. +void tuh_audio_descriptor_cb(uint8_t idx, const tuh_audio_descriptor_cb_t *desc_cb_data); + +// Invoked when device with Audio interface is mounted +void tuh_audio_mount_cb(uint8_t idx); + +// Invoked when device with Audio interface is un-mounted +void tuh_audio_umount_cb(uint8_t idx); + +// Invoked when an isochronous IN transfer completes successfully: the +// received data is already queued into the stream's capture FIFO. +void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes); + +// Invoked after a successful isochronous OUT transfer, before the next packet +// is prepared. After this callback returns, the driver submits queued audio +// from the stream FIFO, or silence when a complete packet is unavailable. +void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes); + +// Reports completion of asynchronous start/stop operations and unrecoverable +// transfer failures. START_COMPLETE is emitted after the complete activation +// sequence and initial endpoint transfers are submitted. XFER_FAILED means the +// HCD could not submit a transfer or completed it unsuccessfully; it is not a +// notification for an individual dropped isochronous packet. The driver stops +// the stream before reporting START_COMPLETE failure or XFER_FAILED. +void tuh_audio_event_cb(uint8_t idx, uint8_t stream_idx, tuh_audio_event_t event, tusb_xfer_result_t result); + +//--------------------------------------------------------------------+ +// Internal Class Driver API +//--------------------------------------------------------------------+ +bool audioh_init(void); +bool audioh_deinit(void); +uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len); +bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num); +bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes); +void audioh_close(uint8_t daddr); + +#ifdef __cplusplus +} +#endif + +#endif /* TUSB_AUDIO_HOST_H_ */ |
