// USB-C Hub with Audio and HID // Based on the TinyUSB example for ESP32-S3 #include "esp_log.h" #include #include #include "sdkconfig.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_chip_info.h" #include "esp_system.h" #include "esp_private/usb_phy.h" #include "tusb.h" #include "class/hid/hid.h" #include "class/audio/audio_device.h" #include "class/audio/audio.h" #include "driver/gpio.h" #define USB_TIMEOUT_MS 5000 #define AUDIO_CTRL_UNDEFINED 0x00 #define AUDIO_CTRL_MUTE 0x01 #define AUDIO_CTRL_VOLUME 0x02 #define AUDIO_CTRL_CS_SAM_FREQ_CONTROL 0x01 #define ENTITY_ID_CLOCK_SOURCE 0x07 // Use the actual value from your descriptor #define ENTITY_ID_FEATURE_UNIT 0x02 // Adjust to match your descriptor #define CHANNEL_MASTER 0x00 // Volume range: -60 dB to 0 dB in 1 dB steps (unit = 1/256 dB) #define VOLUME_CUR_DB (-30) // e.g., -30 dB #define VOLUME_MIN_DB (-60) #define VOLUME_MAX_DB (0) #define VOLUME_RES_DB (1) #define AUDIO_CS_REQ_SET_CUR 0x01 // host → device #define AUDIO_CS_REQ_CUR 0x81 // device → host #ifndef AUDIO_CS_REQ_MIN #define AUDIO_CS_REQ_MIN 0x82 #endif #ifndef AUDIO_CS_REQ_MAX #define AUDIO_CS_REQ_MAX 0x83 #endif #ifndef AUDIO_CS_REQ_RES #define AUDIO_CS_REQ_RES 0x84 #endif #define BUTTON_GPIO 46 #define DEBOUNCE_MS 50 TickType_t last_usb_activity_tick = 0; // Global definition static usb_phy_handle_t phy_hdl; static const char *TAG = "MAIN"; static uint16_t _desc_str[32]; uint8_t data[4] = {0x00, 0x01, 0x00, 0x00}; volatile bool usb_mounted = false; volatile bool usb_needs_restart = false; volatile bool usb_suspended = false; int64_t last_successful_setup_time = 0; static int16_t volume_cur = VOLUME_CUR_DB * 256; // -7680 static const int16_t volume_min = VOLUME_MIN_DB * 256; static const int16_t volume_max = VOLUME_MAX_DB * 256; static const int16_t volume_res = VOLUME_RES_DB * 256; const uint8_t usages[] = { 0x01, // Mute 0x01, // Play/Pause 0x01, // Volume Up 0xEA, // Volume Down 0x68, // F13 0x69, // F14 0x6A // F15 }; const char *string_desc_arr[] = { // String descriptor table //(const char[]){0x04, 0x09}, // 0: English (0x0409) // Unused "String1", // 1 "String2", // 2 "String3", // 3 "String4", // 4 "String5" // 5 }; #if defined(HID_ONLY) #define ITF_NUM_HID 0 #define ITF_NUM_TOTAL 1 #define EPNUM_HID 0x81 // HID endpoint // Device Descriptor tusb_desc_device_t const desc_device = { .bLength = sizeof(tusb_desc_device_t), .bDescriptorType = TUSB_DESC_DEVICE, .bcdUSB = 0x0200, .bDeviceClass = 0x00, .bDeviceSubClass = 0x00, .bDeviceProtocol = 0x00, .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, .idVendor = 0xCafe, .idProduct = 0x4000, .bcdDevice = 0x0100, .iManufacturer = 0x01, .iProduct = 0x02, .iSerialNumber = 0x03, .bNumConfigurations = 0x01 }; const uint8_t desc_hid_report[] = { 0x06, 0x00, 0xFF, // Usage Page (Vendor Defined) 0x09, 0x01, // Usage (Vendor Usage 1) 0xA1, 0x01, // Collection (Application) 0x09, 0x01, // Usage (Vendor Usage 1) 0x15, 0x00, // Logical Minimum (0) 0x26, 0xFF, 0x00, // Logical Maximum (255) 0x75, 0x08, // Report Size (8) 0x95, 0x40, // Report Count (64) 0x81, 0x02, // Input (Data,Var,Abs) 0x09, 0x01, // Usage (Vendor Usage 1) 0x91, 0x02, // Output (Data,Var,Abs) 0xC0 // End Collection }; #define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN) const uint8_t desc_configuration[] = { TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0, 100), TUD_HID_DESCRIPTOR(ITF_NUM_HID, 0, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 10) }; #else #define ITF_NUM_AUDIO_CONTROL 0 #define ITF_NUM_AUDIO_STREAM_MIC 1 #define ITF_NUM_AUDIO_STREAM_SPK 2 #define ITF_NUM_HID 3 #define ITF_NUM_TOTAL 4 #define EPNUM_AUDIO_OUT 0x01 // speaker endpoint #define EPNUM_AUDIO_IN 0x81 // microphone endpoint #define EPNUM_HID 0x82 // HID endpoint #define EPNUM_AUDIO_FB 0x83 // feedback endpoint // HID Report Descriptor #define CHIP_FEATURE_USB_OTG BIT(4) uint8_t const desc_hid_report[] = { 0x05, 0x0C, // Usage Page (Consumer Devices) 0x09, 0x01, // Usage (Consumer Control) 0xA1, 0x01, // Collection (Application) 0x85, 0x01, // Report ID 1 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x09, 0xE9, // Usage (Volume Up) 0x75, 0x01, // Report Size (1) 0x95, 0x01, // Report Count (1) 0x81, 0x02, // Input (Data, Var, Abs) 0x75, 0x07, // Report Size (padding) 0x95, 0x01, // Report Count (1) 0x81, 0x03, // Input (Const, Var, Abs) 0xC0 // End Collection }; /* // COTS HID Report Descriptor required for application uint8_t const desc_hid_report[] = { 0x05, 0x0C, // USAGE_PAGE (Consumer Devices) 0x09, 0x01, // USAGE (Consumer Control) 0xA1, 0x01, // COLLECTION (Application) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x09, 0xE9, // USAGE (Volume Up) 0x09, 0xEA, // USAGE (Volume Down) 0x75, 0x01, // REPORT_SIZE (1) 0x95, 0x02, // REPORT_COUNT (2) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x09, 0xE2, // USAGE (Mute) 0x09, 0x00, // USAGE (undefined) 0x81, 0x06, // INPUT (Data,Var,Rel) 0x05, 0x0B, // USAGE_PAGE (Telephony Devices) 0x09, 0x20, // USAGE (Hook Switch) 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x06, // INPUT (Data,Var,Rel) 0x09, 0x00, // USAGE (undefined) 0x95, 0x03, // REPORT_COUNT (3) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM (255) 0x09, 0x00, // USAGE (undefined) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x03, // REPORT_COUNT (3) 0x81, 0x02, // INPUT (Data,Var,Abs) 0xC0 // END_COLLECTION }; */ tusb_desc_device_t const desc_device = { .bLength = sizeof(tusb_desc_device_t), .bDescriptorType = TUSB_DESC_DEVICE, .bcdUSB = 0x0200, .bDeviceClass = TUSB_CLASS_MISC, .bDeviceSubClass = MISC_SUBCLASS_COMMON, .bDeviceProtocol = MISC_PROTOCOL_IAD, .bMaxPacketSize0 = 64, .idVendor = 0x16C0, .idProduct = 0x05DF, .bcdDevice = 0x0102, .iManufacturer = 0x01, .iProduct = 0x02, .iSerialNumber = 0x03, .bNumConfigurations = 0x01 }; #define AUDIO_CS_INTERFACE 0x24 #define AUDIO_PROTOCOL_V1 0x00 #define AUDIO_CONTROL_HEADER 0x01 #define AUDIO_CONTROL_INPUT_TERMINAL 0x02 #define AUDIO_CONTROL_OUTPUT_TERMINAL 0x03 #define CONFIG_TOTAL_LEN 268 static const uint8_t desc_configuration[] = { //1. Configuration Descriptor //Check TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0xA0, 100), // 9 bytes //2. Interface Association Descriptor: Audio //Check // 8 bytes 0x08, 0x0B, 0x00, 0x03, 0x01, 0x00, 0x20, 0x04, //3. Interface Descriptor (0,0): Class: Audio //Check 0x09, // bLength // 9 bytes TUSB_DESC_INTERFACE, // bDescriptorType ITF_NUM_AUDIO_CONTROL, // bInterfaceNumber 0x00, // bAlternateSetting 0x00, // bNumEndpoints TUSB_CLASS_AUDIO, // bInterfaceClass AUDIO_SUBCLASS_CONTROL, // bInterfaceSubClass 0x20, // bInterfaceProtocol 0x04, // iInterface //4. Audio Control Interface Header Descriptor (Class-Specific) //Check // 9 bytes 0x09, 0x24, 0x01, 0x00, 0x02, 0x04, 0x6B, 0x00, 0x00, //0x09, // bLength //AUDIO_CS_INTERFACE, // bDescriptorType //AUDIO_CONTROL_HEADER, // bDescriptorSubtype //U16_TO_U8S_LE(0x0100), // bcdADC (1.0) //U16_TO_U8S_LE(52), // wTotalLength //0x02, // bInCollection //ITF_NUM_AUDIO_STREAM_MIC, // baInterfaceNr(1) //ITF_NUM_AUDIO_STREAM_SPK, // baInterfaceNr(2) //5. unrecognized class specific Descriptor (Clock Source descriptor?) //Check 0x08, AUDIO_CS_INTERFACE, 0x0A, 0x07, 0x01, 0x01, 0x00, 0x04, // Unrecognized Audio Class-Specific Descriptor // 8 bytes //6. Audio Control Input Terminal descriptor //Check 0x11, AUDIO_CS_INTERFACE, AUDIO_CONTROL_INPUT_TERMINAL, // 17 bytes 0x01, // bTerminalID U16_TO_U8S_LE(0x0101), // TerminalType = Microphone 0x00, // No assoc terminal 0x07, // 1 channel 0x02, 0x00, // Mono config 0x00, // No channel names 0x00, // iTerminal 0x00, // 0x00, // 0x00, // 0x00, // 0x04, // //7. Audio Control Feature Unit descriptor //Check // 18 bytes 0x12, AUDIO_CS_INTERFACE, 0x06, 0x02, 0x01, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, //8. Audio Control Output Terminal descriptor 0x0C, AUDIO_CS_INTERFACE, AUDIO_CONTROL_OUTPUT_TERMINAL, // 12 bytes 0x03, // bTerminalID U16_TO_U8S_LE(0x0301), // TerminalType = USB Streaming 0x00, // No assoc terminal 0x02, // bSourceID (above input terminal) 0x07, // iTerminal 0x00, // 0x00, // 0x04, // //9. Audio Control Input Terminal descriptor //Check 0x11, AUDIO_CS_INTERFACE, AUDIO_CONTROL_INPUT_TERMINAL, // 17 bytes 0x04, // bTerminalID U16_TO_U8S_LE(0x0201), // TerminalType = Microphone 0x00, // No assoc terminal 0x07, // 1 channel 0x01, 0x00, // Mono config 0x00, // No channel names 0x00, // iTerminal 0x00, // 0x00, // 0x00, // 0x00, // 0x04, // //10. Audio Control Feature Unit descriptor //Check // 14 bytes 0x0E, AUDIO_CS_INTERFACE, 0x06, 0x05, 0x04, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, //11. Audio Control Speaker Output Terminal descriptor //Check 0x0C, AUDIO_CS_INTERFACE, AUDIO_CONTROL_OUTPUT_TERMINAL, // 12 bytes 0x06, // U16_TO_U8S_LE(0x0101), // 0x00, // 0x05, // 0x07, // 0x00, // 0x00, // 0x04, // //12. Interface Descriptor 1/0 (Audio, 0 Endpoints) //Check // 9 bytes 0x09, 0x04, 0x01, 0x00, 0x00, 0x01, 0x02, 0x20, 0x04, //13 Interface Descriptor 1/1 (Audio, 1 Endpoint) //Check // 9 bytes 0x09, 0x04, 0x01, 0x01, 0x01, 0x01, 0x02, 0x20, 0x04, // Audio Streaming Interface Descriptor //Check 0x10, 0x24, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x04, // 16 Bytes // Audio Streaming Format Type Descriptor //Check 0x06, 0x24, 0x02, 0x01, 0x02, 0x10, // 6 Bytes // Endpoint Descriptor 01 (1 Out, Isochronous, 1ms) //Check 0x07, 0x05, 0x01, 0x09, 0xC4, 0x00, 0x01, // 7 Bytes // Audio Streaming Isochronous Audio Data Endpoint Desciptor //Check 0x08, 0x25, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // 8 Bytes // Interface Descriptor 2/0 (Audio, 0 Endpoints) //Check 0x09, 0x04, 0x02, 0x00, 0x00, 0x01, 0x02, 0x20, 0x04, // 9 Bytes // Interface Descriptor 2/1 (Audio, 1 Endpoint) //Check 0x09, 0x04, 0x02, 0x01, 0x01, 0x01, 0x02, 0x20, 0x04, // 9 Bytes // Audio Streaming Interface Desciptor //Check 0x10, 0x24, 0x01, 0x06, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x04, // 16 Bytes // Audio Streaming Format Type Descriptor //Check 0x06, 0x24, 0x02, 0x01, 0x02, 0x10, // 6 Bytes // Endpoint Descriptor 81 (1 In, Isochronous, 1ms) //Check 0x07, 0x05, 0x81, 0x05, 0x62, 0x00, 0x01, // 7 Bytes // Audio Streaming Isochronous Audio Data Endpoint Desciptor //Check 0x08, 0x25, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // 8 Bytes // HID Descriptor //Check //0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x34, 0x00, TUD_HID_DESCRIPTOR(ITF_NUM_HID, 5, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 2), // 9 Bytes }; #endif void usb_task(void* param) { ESP_LOGI(TAG, "USB Task Start..."); while(1){ tud_task(); // tinyusb device task vTaskDelay(pdMS_TO_TICKS(1)); // 100ms vertraging //ESP_LOGI(TAG, "Tud Task Busy..."); } }; void hid_task(void* param) { bool last_state = gpio_get_level(BUTTON_GPIO); // Initial state uint8_t data[4] = {0x00, last_state ? 0x01 : 0x00, 0x00, 0x00}; bool stable_state = last_state; // Current stable read TickType_t last_change_tick = xTaskGetTickCount(); ESP_LOGI(TAG, "HID Task Start..."); while (1) { if (tud_hid_ready()) { // Check if HID is ready to send report int current_level = gpio_get_level(BUTTON_GPIO); if (current_level != stable_state) { // Level changed, reset debounce timer stable_state = current_level; last_change_tick = xTaskGetTickCount(); } if ((xTaskGetTickCount() - last_change_tick) > pdMS_TO_TICKS(DEBOUNCE_MS)) { // If stable for debounce period and state differs from last reported if (stable_state != last_state) { last_state = stable_state; data[1] = stable_state ? 0x01 : 0x00; // HID report should be "00 00 00 00" when nothing is pressed or "00 01 00 00" when PTT is pressed ESP_LOGI(TAG, "HID about to send report (data[1]: 0x%02X)", data[1]); tud_hid_report(0, data, sizeof(data)); // Original HID send Report ESP_LOGI(TAG, "HID message SEND"); /* // Test for Volume Up command ************************** uint8_t report[2] = {0x01, 0x01}; // Report ID = 1, Volume Up = pressed tud_hid_report(report[0], report, sizeof(report)); vTaskDelay(pdMS_TO_TICKS(100)); // Release volume up report[1] = 0x00; tud_hid_report(report[0], report, sizeof(report)); vTaskDelay(pdMS_TO_TICKS(50)); // Send empty report to flush lingering state (esp. Android) Else volume up is always pressed. tud_hid_report(report[0], report, sizeof(report)); // Test for Volume Up command ************************** */ } } } else { ESP_LOGI(TAG, "HID not ready"); } vTaskDelay(pdMS_TO_TICKS(10)); // Poll every 10ms } } bool usb_init(void) { // Configure USB PHY usb_phy_config_t phy_conf = { .controller = USB_PHY_CTRL_OTG, .target = USB_PHY_TARGET_INT, // maybe we can use USB_OTG_MODE_DEFAULT and switch using dwc2 driver #if CFG_TUD_ENABLED .otg_mode = USB_OTG_MODE_DEVICE, #elif CFG_TUH_ENABLED .otg_mode = USB_OTG_MODE_HOST, #endif // https://github.com/hathach/tinyusb/issues/2943#issuecomment-2601888322 // Set speed to undefined (auto-detect) to avoid timinng/racing issue with S3 with host such as macOS .otg_speed = USB_PHY_SPEED_UNDEFINED, }; usb_new_phy(&phy_conf, &phy_hdl); return true; } void usb_monitor_task(void *param) { while (1) { if (usb_needs_restart) { ESP_LOGI("USB", "Restarting TinyUSB from suspend..."); tusb_init(); usb_needs_restart = false; } vTaskDelay(pdMS_TO_TICKS(500)); } } void tud_sof_cb(uint32_t frame_count) { last_usb_activity_tick = xTaskGetTickCount(); } void usb_watchdog_task(void *param) { bool usb_was_active = false; last_usb_activity_tick = xTaskGetTickCount(); while (1) { TickType_t now = xTaskGetTickCount(); if (tud_mounted() && !tud_suspended()) { // Als we al activiteit hebben gezien if (usb_was_active) { uint32_t inactive_time_ms = (now - last_usb_activity_tick) * portTICK_PERIOD_MS; if (inactive_time_ms > USB_TIMEOUT_MS) { ESP_LOGW("USB", "⚠️ USB lijkt inactief (%lu ms) — herstart TinyUSB stack...", inactive_time_ms); vTaskDelay(pdMS_TO_TICKS(500)); tusb_init(); last_usb_activity_tick = now; usb_was_active = false; } } } vTaskDelay(pdMS_TO_TICKS(1000)); } } void app_main(void) { gpio_config_t io_conf = { // Initialize the Pushbutton (GPIO46) to Simulate PTT button .pin_bit_mask = 1ULL << BUTTON_GPIO, .mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_ENABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE }; gpio_config(&io_conf); esp_chip_info_t info; esp_chip_info(&info); ESP_LOGI(TAG, "Chip features: %s", (info.features & CHIP_FEATURE_USB_OTG) ? "USB OTG" : "No USB"); ESP_LOGI(TAG, "Config descriptor size: %d", sizeof(desc_configuration)); ESP_LOGI(TAG, "HID report size: %d", sizeof(desc_hid_report)); usb_init(); //Initialize the USB peripheral vTaskDelay(pdMS_TO_TICKS(500)); tusb_init(); // Initialize the TinyUSB Stack vTaskDelay(pdMS_TO_TICKS(100)); ESP_LOGI(TAG, "Tusb Init..."); //vTaskDelay(pdMS_TO_TICKS(100)); xTaskCreate(usb_task, "usb_task", 4096, NULL, 5, NULL); // Create Multiple Tasks xTaskCreate(hid_task, "hid_task", 4096, NULL, 5, NULL); xTaskCreate(usb_monitor_task, "usb_monitor", 2048, NULL, 5, NULL); xTaskCreate(usb_watchdog_task, "usb_watchdog", 4096, NULL, 5, NULL); esp_chip_info(&info); ESP_LOGI(TAG, "Chip features: %s", (info.features & CHIP_FEATURE_USB_OTG) ? "USB OTG" : "No USB"); while (1) { vTaskDelay(pdMS_TO_TICKS(100)); // Delay the main loop so prevent craches. } } uint8_t const *tud_descriptor_device_cb(void) { return (uint8_t const *) &desc_device; } uint8_t const* tud_descriptor_configuration_cb(uint8_t index) { (void) index; return desc_configuration; } uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) { (void)langid; uint8_t chr_count; if (index == 0) { // Supported language = English (0x0409) _desc_str[1] = 0x0409; _desc_str[0] = (TUSB_DESC_STRING << 8) | (2 + 2); // 2 bytes header + 2 for langID return _desc_str; } if (index < 1 || index > sizeof(string_desc_arr) / sizeof(string_desc_arr[0])) { return NULL; } const char* str = string_desc_arr[index - 1]; chr_count = strlen(str); if (chr_count > 31) chr_count = 31; for (uint8_t i = 0; i < chr_count; i++) { _desc_str[1 + i] = (uint16_t)str[i]; // ASCII to UTF-16LE } _desc_str[0] = (TUSB_DESC_STRING << 8) | (2 * chr_count + 2); return _desc_str; } uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance) { (void) instance; return desc_hid_report; } uint16_t tud_hid_descriptor_report_size_cb(uint8_t instance) { (void)instance; return sizeof(desc_hid_report); } // Called when the host requests a HID report (GET_REPORT) uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen) { // Return the length of the report copied to buffer, or zero if not supported uint8_t report_len = 8; // Match your endpoint size if (report_type == HID_REPORT_TYPE_INPUT && reqlen >= report_len) { memset(buffer, 0, report_len); return report_len; } return 0; } // Called when the host sends a HID report (SET_REPORT) void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) { // Handle received report from host (if needed) } void audiod_init(void) { } bool audiod_deinit(void) { return true; } void audiod_reset(uint8_t rhport) { (void) rhport; } uint16_t audiod_open(uint8_t rhport, tusb_desc_interface_t const* itf_desc, uint16_t max_len) { (void) rhport; uint8_t const* p = (uint8_t const*) itf_desc; uint16_t len = 0; while (len < max_len) { uint8_t bLength = p[0]; uint8_t bDescriptorType = p[1]; if (bLength == 0) break; // Stop parsing if we hit another interface beyond our known range if (bDescriptorType == TUSB_DESC_INTERFACE) { const tusb_desc_interface_t* desc = (const tusb_desc_interface_t*) p; // Only process AUDIO interfaces if (desc->bInterfaceClass != TUSB_CLASS_AUDIO) { break; } ESP_LOGI("AUDIOD", "Claiming Interface #%d, subclass 0x%02X", desc->bInterfaceNumber, desc->bInterfaceSubClass); } p += bLength; len += bLength; } ESP_LOGI("AUDIOD", "Claimed %d bytes of audio interfaces", len); return len; } bool audiod_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) { if (stage != CONTROL_STAGE_SETUP) return true; ESP_LOGI("AUDIOD", "Control Req: bRequest=0x%02X, wIndex=0x%04X, wValue=0x%04X, wLength=%d", request->bRequest, request->wIndex, request->wValue, request->wLength); // Decode fields uint8_t entity_id = (uint8_t)(request->wIndex >> 8); uint8_t interface = (uint8_t)(request->wIndex & 0xFF); uint8_t channel = (uint8_t)(request->wValue & 0xFF); uint8_t control_selector = (uint8_t)(request->wValue >> 8); static uint32_t sample_rate = 48000; static uint8_t mute = 0; static uint8_t mute_0x02 = 0; static int16_t vol = tu_htole16(-128); // of 0 dB ESP_LOGI("AUDIOD", "Entity ID: 0x%02X, Interface: %d, Channel: %d, Control: 0x%02X", entity_id, interface, channel, control_selector); if (request->bRequest == 0x0B) { return false; } // --- Handle volume GET_RANGE --- if (request->bRequest == AUDIO_CS_REQ_RANGE && control_selector == AUDIO_CTRL_VOLUME && request->wLength >= 8) { // Host expects 1 subrange static struct __attribute__((packed)) { uint16_t wNumSubRanges; struct { int16_t min; int16_t max; int16_t res; } range; } volume_range = { .wNumSubRanges = tu_htole16(1), .range = { .min = tu_htole16(-100), // -100 / 256 = -0.39 dB .max = tu_htole16(0), .res = tu_htole16(1) } }; ESP_LOGI("AUDIOD", "Sending volume range descriptor (%d bytes)", sizeof(volume_range)); return tud_control_xfer(rhport, request, &volume_range, sizeof(volume_range)); } if (request->bRequest == AUDIO_CS_REQ_CUR && entity_id == 0x07 && control_selector == AUDIO_CTRL_VOLUME){ if (request->wLength == 2) { static int16_t vol = tu_htole16(-128); // -0.5 dB return tud_control_xfer(rhport, request, &vol, sizeof(vol)); } else if (request->wLength == 4) { static int32_t vol32 = tu_htole32(-128); // zelfde waarde return tud_control_xfer(rhport, request, &vol32, sizeof(vol32)); } } // Sample Frequency GET_RANGE (on clock source) if (request->bRequest == AUDIO_CS_REQ_RANGE && entity_id == ENTITY_ID_CLOCK_SOURCE && control_selector == AUDIO_CTRL_CS_SAM_FREQ_CONTROL && request->wLength >= 14) { static struct __attribute__((packed)) { uint16_t wNumSubRanges; struct { uint32_t min; uint32_t max; uint32_t res; } range; } sample_freq_range = { .wNumSubRanges = 1, .range = { .min = 48000, .max = 48000, .res = 0 } }; ESP_LOGI("AUDIOD", "Sending sample frequency range"); return tud_control_xfer(rhport, request, &sample_freq_range, sizeof(sample_freq_range)); } // Only respond to Feature Unit Volume GET_CUR if (request->bRequest == AUDIO_CS_REQ_CUR && control_selector == AUDIO_CTRL_VOLUME && entity_id == ENTITY_ID_FEATURE_UNIT && // define this correctly! request->wLength == 2) { // Example: -0.355 dB = -91 (int16_t) static const uint8_t vol[2] = { 0xA5, 0xFF }; // Little-endian return tud_control_xfer(rhport, request, (void*)vol, sizeof(vol)); } if (request->bRequest == AUDIO_CS_REQ_CUR && control_selector == AUDIO_CTRL_MUTE && entity_id == ENTITY_ID_FEATURE_UNIT) { static uint8_t mute = 0x00; return tud_control_xfer(rhport, request, &mute, sizeof(mute)); } if (request->bRequest == AUDIO_CS_REQ_CUR && control_selector == AUDIO_CTRL_VOLUME && entity_id == 0x05 /* or your other unit */) { static int16_t vol = tu_htole16(0); // 0 dB return tud_control_xfer(rhport, request, &vol, sizeof(vol)); } if (request->bRequest == AUDIO_CS_REQ_CUR && control_selector == AUDIO_CTRL_MUTE && entity_id == 0x05) { static uint8_t mute = 0x00; return tud_control_xfer(rhport, request, &mute, sizeof(mute)); } if (request->bRequest == AUDIO_CS_REQ_SET_CUR && entity_id == 0x07 && control_selector == AUDIO_CTRL_CS_SAM_FREQ_CONTROL && request->wLength == sizeof(sample_rate)){ return tud_control_xfer(rhport, request, &sample_rate, sizeof(sample_rate)); } if (request->bRequest == AUDIO_CS_REQ_SET_CUR && control_selector == AUDIO_CTRL_MUTE && entity_id == 0x02 && request->wLength == 1){ return tud_control_xfer(rhport, request, &mute_0x02, sizeof(mute_0x02)); } if (request->bRequest == AUDIO_CS_REQ_CUR && control_selector == AUDIO_CTRL_MUTE && entity_id == 0x02 && request->wLength == 1){ return tud_control_xfer(rhport, request, &mute_0x02, sizeof(mute_0x02)); } if (request->bRequest == AUDIO_CS_REQ_SET_CUR && control_selector == AUDIO_CTRL_MUTE && entity_id == 0x05 && request->wLength == 1){ return tud_control_xfer(rhport, request, &mute, sizeof(mute)); } if (request->bRequest == AUDIO_CS_REQ_SET_CUR && control_selector == AUDIO_CTRL_VOLUME && entity_id == 0x05 && request->wLength == 2){ return tud_control_xfer(rhport, request, &vol, sizeof(vol)); } if (entity_id == ENTITY_ID_FEATURE_UNIT && control_selector == AUDIO_CTRL_VOLUME && channel == CHANNEL_MASTER && request->wLength == 2){ switch (request->bRequest) { case AUDIO_CS_REQ_CUR: // Host is asking for current volume return tud_control_xfer(rhport, request, &volume_cur, sizeof(volume_cur)); case AUDIO_CS_REQ_MIN: return tud_control_xfer(rhport, request, (void*)&volume_min, sizeof(volume_min)); case AUDIO_CS_REQ_MAX: return tud_control_xfer(rhport, request, (void*)&volume_max, sizeof(volume_max)); case AUDIO_CS_REQ_RES: return tud_control_xfer(rhport, request, (void*)&volume_res, sizeof(volume_res)); case AUDIO_CS_REQ_SET_CUR: // Host wants to set the volume return tud_control_xfer(rhport, request, &volume_cur, sizeof(volume_cur)); // TinyUSB will write to this on completion default: return false; // Unsupported request } } ESP_LOGI("AUDIOD", "No match, STALL (entity=0x%02X, ctrl=0x%02X, bReq=0x%02X)", entity_id, control_selector, request->bRequest); // Unsupported -> stall return false; } bool audiod_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { (void) rhport; (void) ep_addr; (void) result; (void) xferred_bytes; return true; } void audiod_sof_isr(uint8_t rhport, uint32_t frame_count) { (void) rhport; (void) frame_count; } bool tud_audio_rx_done_cb(uint8_t itf, uint8_t const* buffer, uint16_t bufsize) { ESP_LOGI("USB", "Speaker received %d bytes", bufsize); return true; } bool tud_audio_tx_done_cb(uint8_t itf, uint8_t* buffer, uint16_t bufsize) { // For mic-output (IN to host) memset(buffer, 0, bufsize); return true; } void tud_hid_report_complete_cb(uint8_t instance, uint8_t const* report, uint16_t len) { last_usb_activity_tick = xTaskGetTickCount(); // USB Activity detected } void tud_mount_cb(void) { ESP_LOGI("USB", "Device successfully mounted!"); usb_mounted = true; } void tud_umount_cb(void) { ESP_LOGI("USB", "🚫 Device unmounted."); } void tud_suspend_cb(bool remote_wakeup_en) { ESP_LOGI("USB", "💤 USB Suspended. Remote wakeup: %d", remote_wakeup_en); } void tud_resume_cb(void) { ESP_LOGI("USB", "🔄 USB Resume."); }