espressif / esp-idf

Espressif IoT Development Framework. Official development framework for Espressif SoCs.
Apache License 2.0
13.82k stars 7.32k forks source link

Basic ESP32-S3 TinyUSB Audio Device Example? (IDFGH-11661) #12774

Open akumpf opened 11 months ago

akumpf commented 11 months ago

Is your feature request related to a problem?

Being able to route lossless audio to/from a computer is super helpful for debugging a very wide range of audio projects (audio effects, speech, AI, synthesizers, etc.).

The underlying TinyUSB library includes support for UAC2 audio, and ESP32-S2/S3 should be able to handle this just fine. But it doesn't look like the ESP-IDF + esp_tinyusb component supports it yet. What would it take to port over the basic TinyUSB "audio_test" example? Or should esp_tinyusb not be used at all, and just directly use tinyusb?

Describe the solution you'd like.

So far, it looks like we'll need:

For the example, I think it could be limited to a single audio channel (like the "one channel mic" used in the example) at a fixed samplerate (like 48000).

Describe alternatives you've considered.

I've been able to compile the TinyUSB audio_test example and upload to an ESP32-S3, but the USB audio interface doesn't show up. I'm guessing it has to do with some specific low-level knowledge of USB descriptors/limitations, so hoping that someone more familiar with USB it may be able to highlight the important flags/settings to get things up and running with USB audio.

Additional context.

No response

akumpf commented 11 months ago

For completeness, here's the code I've been using to test USB UAC2 audio. As mentioned, it compiles/uploads fine but doesn't actually show us as a USB audio device on the host computer.

// ----------------------------------------------
static const char *TAG_USB = "usb";
// ----------------------------------------------
// Force AUDIO since no flag currently in Kconfig/menuconfig...
#define CFG_TUD_AUDIO   1 
// ----------------------------------------------
// see: https://github.com/hathach/tinyusb/blob/master/examples/device/audio_test/src/tusb_config.h
#define CFG_TUD_AUDIO_FUNC_1_DESC_LEN                                 TUD_AUDIO_MIC_ONE_CH_DESC_LEN
#define CFG_TUD_AUDIO_FUNC_1_N_AS_INT                                 1                                       // Number of Standard AS Interface Descriptors (4.9.1) defined per audio function - this is required to be able to remember the current alternate settings of these interfaces - We restrict us here to have a constant number for all audio functions (which means this has to be the maximum number of AS interfaces an audio function has and a second audio function with less AS interfaces just wastes a few bytes)
#define CFG_TUD_AUDIO_FUNC_1_CTRL_BUF_SZ                              64                                      // Size of control request buffer
#define CFG_TUD_AUDIO_ENABLE_EP_IN                                    1
#define CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX                    2                                       // Driver gets this info from the descriptors - we define it here to use it to setup the descriptors and to do calculations with it below
#define CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX                            1                                       // Driver gets this info from the descriptors - we define it here to use it to setup the descriptors and to do calculations with it below - be aware: for different number of channels you need another descriptor!
#define CFG_TUD_AUDIO_EP_SZ_IN                                        (48+1) * CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX * CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX      // 48 Samples (48 kHz) x 2 Bytes/Sample x CFG_TUD_AUDIO_N_CHANNELS_TX Channels - One extra sample is needed for asynchronous transfer adjustment, see feedback EP
#define CFG_TUD_AUDIO_FUNC_1_EP_IN_SZ_MAX                             CFG_TUD_AUDIO_EP_SZ_IN                  // Maximum EP IN size for all AS alternate settings used
#define CFG_TUD_AUDIO_FUNC_1_EP_IN_SW_BUF_SZ                          CFG_TUD_AUDIO_EP_SZ_IN + 1
// ---- 
#include <stdlib.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_timer.h"
#include "tinyusb.h"
// ---- 
// Interface counter
enum usb_interface_count {
  ITF_NUM_AUDIO_CONTROL = 0,
  ITF_NUM_AUDIO_STREAMING_MIC,
  ITF_COUNT
};
// USB Endpoint numbers: Available USB Endpoints: 5 IN/OUT EPs and 1 IN EP
#define USB_EPNUM_AUDIO     0x01

//--------------------------------------------------------------------+
// Device Descriptors
//--------------------------------------------------------------------+
#define _PID_MAP(itf, n)  ( (CFG_TUD_##itf) << (n) )
#define USB_PID           (0x4000 | _PID_MAP(CDC, 0) | _PID_MAP(MSC, 1) | _PID_MAP(HID, 2) | _PID_MAP(MIDI, 3) | _PID_MAP(AUDIO, 4) | _PID_MAP(VENDOR, 5) )
tusb_desc_device_t const usb_device_desc ={
    .bLength            = sizeof(tusb_desc_device_t),
    .bDescriptorType    = TUSB_DESC_DEVICE,
    .bcdUSB             = 0x0200,
    // Use Interface Association Descriptor (IAD) for Audio
    // As required by USB Specs IAD's subclass must be common class (2) and protocol must be IAD (1)
    .bDeviceClass       = TUSB_CLASS_MISC,
    .bDeviceSubClass    = MISC_SUBCLASS_COMMON,
    .bDeviceProtocol    = MISC_PROTOCOL_IAD,
    .bMaxPacketSize0    = CFG_TUD_ENDPOINT0_SIZE,
    .idVendor           = 0xCafe,
    .idProduct          = USB_PID,
    .bcdDevice          = 0x0100,
    .iManufacturer      = 0x01,
    .iProduct           = 0x02,
    .iSerialNumber      = 0x03,
    .bNumConfigurations = 0x01
};
// TinyUSB descriptors
#define TUSB_DESCRIPTOR_TOTAL_LEN (TUD_CONFIG_DESC_LEN + CFG_TUD_AUDIO * TUD_AUDIO_MIC_ONE_CH_DESC_LEN)

// String descriptor
static const char* usb_string_desc_arr[7] = {
    // array of pointer to string descriptors
    (char[]){0x09, 0x04},   // 0: is supported language is English (0x0409)
    "TinyUSB",              // 1: Manufacturer
    "Product",              // 2: Product
    "12345678",             // 3: Serials, should use chip ID
    "AudioName",            // 4: Audio
};
// --
// Configuration descriptor
static const uint8_t usb_desc_configuration[] = {
  // Configuration number, interface count, string index, total length, attribute, power in mA
  TUD_CONFIG_DESCRIPTOR(1, ITF_COUNT, 0, TUSB_DESCRIPTOR_TOTAL_LEN, 0x00, 100),

  // Audio Mic: Interface number, string index, EP Out & EP In address, EP size
  // Verified that TUD_AUDIO_MIC_ONE_CH_DESC_LEN = 132 = length of result for 'TUD_AUDIO_MIC_ONE_CH_DESCRIPTOR()'. So length is not the issue.
  TUD_AUDIO_MIC_ONE_CH_DESCRIPTOR(ITF_NUM_AUDIO_CONTROL, 4, CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX, CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX*8, 0x80 | USB_EPNUM_AUDIO, CFG_TUD_AUDIO_EP_SZ_IN),
};
//--------------------------------------------------------------------+
// USB Init
//--------------------------------------------------------------------+
void usb_init(){
  tinyusb_config_t const tusb_cfg = {
      .device_descriptor = &usb_device_desc, // If device_descriptor is NULL, tinyusb_driver_install() will use Kconfig
      .string_descriptor = usb_string_desc_arr,
      .string_descriptor_count = sizeof(usb_string_desc_arr) / sizeof(usb_string_desc_arr[0]),
      .external_phy = false, // false = use internal USB interface (i.e. not an external USB chip)
      .configuration_descriptor = usb_desc_configuration,
  };
  ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));
  ESP_LOGI(TAG_USB, "USB initialization DONE");
}

void app_main(void){
  usb_init();
  // --
  while(1){
    ESP_LOGI(TAG_USB, "testing usb audio");
    vTaskDelay(1000 / portTICK_PERIOD_MS); // Handle for low-speed updates at ~100Hz (10ms).
  }
}

//--------------------------------------------------------------------+
// Audio STATE
//--------------------------------------------------------------------+

// Audio controls
// Current states
bool mute[CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX + 1];                        // +1 for master channel 0
uint16_t volume[CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX + 1];                    // +1 for master channel 0
uint32_t sampFreq;
uint8_t clkValid;

// Range states
audio_control_range_2_n_t(1) volumeRng[CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX+1];           // Volume range state
audio_control_range_4_n_t(1) sampleFreqRng;                         // Sample frequency range state

// Audio test data
uint16_t test_buffer_audio[(CFG_TUD_AUDIO_EP_SZ_IN - 2) / 2];
uint16_t startVal = 0;

//--------------------------------------------------------------------+
// Device callbacks
//--------------------------------------------------------------------+
// Invoked when device is mounted
void tud_mount_cb(void){
//   blink_interval_ms = BLINK_MOUNTED;
}

// Invoked when device is unmounted
void tud_umount_cb(void){
//   blink_interval_ms = BLINK_NOT_MOUNTED;
}

// Invoked when usb bus is suspended
// remote_wakeup_en : if host allow us  to perform remote wakeup
// Within 7ms, device must draw an average of current less than 2.5 mA from bus
void tud_suspend_cb(bool remote_wakeup_en){
  (void) remote_wakeup_en;
//   blink_interval_ms = BLINK_SUSPENDED;
}

// Invoked when usb bus is resumed
void tud_resume_cb(void){
//   blink_interval_ms = tud_mounted() ? BLINK_MOUNTED : BLINK_NOT_MOUNTED;
}

//--------------------------------------------------------------------+
// Application Callback API Implementations
//--------------------------------------------------------------------+
// Invoked when audio class specific set request received for an EP
bool tud_audio_set_req_ep_cb(uint8_t rhport, tusb_control_request_t const * p_request, uint8_t *pBuff){
  (void) rhport;
  (void) pBuff;
  // We do not support any set range requests here, only current value requests
  TU_VERIFY(p_request->bRequest == AUDIO_CS_REQ_CUR);
  // Page 91 in UAC2 specification
  uint8_t channelNum = TU_U16_LOW(p_request->wValue);
  uint8_t ctrlSel = TU_U16_HIGH(p_request->wValue);
  uint8_t ep = TU_U16_LOW(p_request->wIndex);
  (void) channelNum; (void) ctrlSel; (void) ep;
  return false;     // Yet not implemented
}

// Invoked when audio class specific set request received for an interface
bool tud_audio_set_req_itf_cb(uint8_t rhport, tusb_control_request_t const * p_request, uint8_t *pBuff){
  (void) rhport;
  (void) pBuff;
  // We do not support any set range requests here, only current value requests
  TU_VERIFY(p_request->bRequest == AUDIO_CS_REQ_CUR);
  // Page 91 in UAC2 specification
  uint8_t channelNum = TU_U16_LOW(p_request->wValue);
  uint8_t ctrlSel = TU_U16_HIGH(p_request->wValue);
  uint8_t itf = TU_U16_LOW(p_request->wIndex);
  (void) channelNum; (void) ctrlSel; (void) itf;
  return false;     // Yet not implemented
}

// Invoked when audio class specific set request received for an entity
bool tud_audio_set_req_entity_cb(uint8_t rhport, tusb_control_request_t const * p_request, uint8_t *pBuff){
  (void) rhport;
  // Page 91 in UAC2 specification
  uint8_t channelNum = TU_U16_LOW(p_request->wValue);
  uint8_t ctrlSel = TU_U16_HIGH(p_request->wValue);
  uint8_t itf = TU_U16_LOW(p_request->wIndex);
  uint8_t entityID = TU_U16_HIGH(p_request->wIndex);
  (void) itf;
  // We do not support any set range requests here, only current value requests
  TU_VERIFY(p_request->bRequest == AUDIO_CS_REQ_CUR);
  // If request is for our feature unit
  if ( entityID == 2 ){
    switch ( ctrlSel ){
      case AUDIO_FU_CTRL_MUTE:
        // Request uses format layout 1
        TU_VERIFY(p_request->wLength == sizeof(audio_control_cur_1_t));
        mute[channelNum] = ((audio_control_cur_1_t*) pBuff)->bCur;
        TU_LOG2("    Set Mute: %d of channel: %u\r\n", mute[channelNum], channelNum);
      return true;
      // --
      case AUDIO_FU_CTRL_VOLUME:
        // Request uses format layout 2
        TU_VERIFY(p_request->wLength == sizeof(audio_control_cur_2_t));
        volume[channelNum] = (uint16_t) ((audio_control_cur_2_t*) pBuff)->bCur;
        TU_LOG2("    Set Volume: %d dB of channel: %u\r\n", volume[channelNum], channelNum);
      return true;
        // Unknown/Unsupported control
      default:
        TU_BREAKPOINT();
      return false;
    }
  }
  return false;    // Yet not implemented
}
// Invoked when audio class specific get request received for an EP
bool tud_audio_get_req_ep_cb(uint8_t rhport, tusb_control_request_t const * p_request){
  (void) rhport;
  // Page 91 in UAC2 specification
  uint8_t channelNum = TU_U16_LOW(p_request->wValue);
  uint8_t ctrlSel = TU_U16_HIGH(p_request->wValue);
  uint8_t ep = TU_U16_LOW(p_request->wIndex);
  (void) channelNum; (void) ctrlSel; (void) ep;
  //    return tud_control_xfer(rhport, p_request, &tmp, 1);
  return false;     // Yet not implemented
}

// Invoked when audio class specific get request received for an interface
bool tud_audio_get_req_itf_cb(uint8_t rhport, tusb_control_request_t const * p_request){
  (void) rhport;
  // Page 91 in UAC2 specification
  uint8_t channelNum = TU_U16_LOW(p_request->wValue);
  uint8_t ctrlSel = TU_U16_HIGH(p_request->wValue);
  uint8_t itf = TU_U16_LOW(p_request->wIndex);
  (void) channelNum; (void) ctrlSel; (void) itf;
  return false;     // Yet not implemented
}

// Invoked when audio class specific get request received for an entity
bool tud_audio_get_req_entity_cb(uint8_t rhport, tusb_control_request_t const * p_request){
  (void) rhport;
  // Page 91 in UAC2 specification
  uint8_t channelNum = TU_U16_LOW(p_request->wValue);
  uint8_t ctrlSel = TU_U16_HIGH(p_request->wValue);
  // uint8_t itf = TU_U16_LOW(p_request->wIndex);           // Since we have only one audio function implemented, we do not need the itf value
  uint8_t entityID = TU_U16_HIGH(p_request->wIndex);
  // Input terminal (Microphone input)
  if (entityID == 1){
    switch ( ctrlSel ){
      case AUDIO_TE_CTRL_CONNECTOR:{
        // The terminal connector control only has a get request with only the CUR attribute.
        audio_desc_channel_cluster_t ret;
        // Those are dummy values for now
        ret.bNrChannels = 1;
        ret.bmChannelConfig = (audio_channel_config_t) 0;
        ret.iChannelNames = 0;
        TU_LOG2("    Get terminal connector\r\n");
        return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, (void*) &ret, sizeof(ret));
      }
      break;
        // Unknown/Unsupported control selector
      default:
        TU_BREAKPOINT();
        return false;
    }
  }
  // Feature unit
  if (entityID == 2){
    switch ( ctrlSel ){
      case AUDIO_FU_CTRL_MUTE:
        // Audio control mute cur parameter block consists of only one byte - we thus can send it right away
        // There does not exist a range parameter block for mute
        TU_LOG2("    Get Mute of channel: %u\r\n", channelNum);
        return tud_control_xfer(rhport, p_request, &mute[channelNum], 1);
      case AUDIO_FU_CTRL_VOLUME:
        switch ( p_request->bRequest ){
          case AUDIO_CS_REQ_CUR:
            TU_LOG2("    Get Volume of channel: %u\r\n", channelNum);
            return tud_control_xfer(rhport, p_request, &volume[channelNum], sizeof(volume[channelNum]));
          case AUDIO_CS_REQ_RANGE:
            TU_LOG2("    Get Volume range of channel: %u\r\n", channelNum);
            // Copy values - only for testing - better is version below
            audio_control_range_2_n_t(1)
            ret;
            ret.wNumSubRanges = 1;
            ret.subrange[0].bMin = -90;    // -90 dB
            ret.subrange[0].bMax = 90;      // +90 dB
            ret.subrange[0].bRes = 1;       // 1 dB steps
            return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, (void*) &ret, sizeof(ret));
            // Unknown/Unsupported control
          default:
            TU_BREAKPOINT();
            return false;
        }
      break;
        // Unknown/Unsupported control
      default:
        TU_BREAKPOINT();
        return false;
    }
  }
  // Clock Source unit
  if ( entityID == 4 ){
    switch ( ctrlSel ){
      case AUDIO_CS_CTRL_SAM_FREQ:
        // channelNum is always zero in this case
        switch ( p_request->bRequest ){
          case AUDIO_CS_REQ_CUR:
            TU_LOG2("    Get Sample Freq.\r\n");
            return tud_control_xfer(rhport, p_request, &sampFreq, sizeof(sampFreq));
          case AUDIO_CS_REQ_RANGE:
            TU_LOG2("    Get Sample Freq. range\r\n");
            return tud_control_xfer(rhport, p_request, &sampleFreqRng, sizeof(sampleFreqRng));
          // Unknown/Unsupported control
          default:
            TU_BREAKPOINT();
            return false;
        }
      break;
      case AUDIO_CS_CTRL_CLK_VALID:
        // Only cur attribute exists for this request
        TU_LOG2("    Get Sample Freq. valid\r\n");
        return tud_control_xfer(rhport, p_request, &clkValid, sizeof(clkValid));
      // Unknown/Unsupported control
      default:
        TU_BREAKPOINT();
        return false;
    }
  }
  TU_LOG2("  Unsupported entity: %d\r\n", entityID);
  return false;     // Yet not implemented
}
bool tud_audio_tx_done_pre_load_cb(uint8_t rhport, uint8_t itf, uint8_t ep_in, uint8_t cur_alt_setting){
  (void) rhport;
  (void) itf;
  (void) ep_in;
  (void) cur_alt_setting;
  tud_audio_write ((uint8_t *)test_buffer_audio, CFG_TUD_AUDIO_EP_SZ_IN - 2);
  return true;
}
bool tud_audio_tx_done_post_load_cb(uint8_t rhport, uint16_t n_bytes_copied, uint8_t itf, uint8_t ep_in, uint8_t cur_alt_setting){
  (void) rhport;
  (void) n_bytes_copied;
  (void) itf;
  (void) ep_in;
  (void) cur_alt_setting;
  for (size_t cnt = 0; cnt < (CFG_TUD_AUDIO_EP_SZ_IN - 2) / 2; cnt++){
    test_buffer_audio[cnt] = startVal++;
  }
  return true;
}
bool tud_audio_set_itf_close_EP_cb(uint8_t rhport, tusb_control_request_t const * p_request){
  (void) rhport;
  (void) p_request;
  startVal = 0;
  return true;
}
roma-jam commented 11 months ago

Hi @akumpf , Thanks for the reported fully described problem.

ESP-IDF uses the esp_tinyusb component which in it's own turn uses the tinyUSB component. Defining the CFG_TUD_AUDIO works only for the first esp_tinyusb extra component (https://components.espressif.com/components/espressif/esp_tinyusb), but right now there is no support of audio device.
Apparently, the define doesn't works for tinyUSB and this value will be redefined later.

That is why you don't see the device, despite the fact that example has been written correctly.

Anyway, the idea to make this possible can be treated as a good addition to the esp_tinyusb idf-extra-component. We add that feature to the plan.

For now there is a two options available:

It is hard to say any particular date right now, as currently we are busy with adding HighSpeed support for upcoming chips. Anyway, pull-requests always appreciated.

akumpf commented 11 months ago

Thanks for the quick follow-up @roma-jam.

Really looking forward to AUDIO being part of esp_tinyusb, but for now I can at least try to play/explore a bit using the TinyUSB component directly. 🤞 😅

roma-jam commented 11 months ago

Hi @akumpf, No problem at all. :+1:

I'm guessing it has to do with some specific low-level knowledge of USB descriptors/limitations

Actually, no. All the descriptors within your example provided correctly. The sources of audio driver in tinyUSB is compiled, so only configuration CFG_TUD_AUDIO is missing. So the changes to make the example work are pretty simple. On the other hand, to make the configuration universal - this might require a little bit more time.

Meanwhile, do you have anything extra that you would like to have as a configurable parameters for AUDIO driver via KConfig?

akumpf commented 11 months ago

The main high-level options I can imagine being helpful for basic USB audio usage are:

But those are all just icing on the cake! 🍰 Getting USB Audio to work with esp-idf in any form would be fantastic.

roma-jam commented 11 months ago

Hi @akumpf, I have a little update. We checked the possibility to support all the configuration, but it will take time.

Right now, the best option will be:

  1. Use the TinyUSB component directly and provide correct tusb_config.h file (please, refer this instruction: https://github.com/espressif/tinyusb/tree/release/v0.15#2-use-tinyusb-only-without-the-additions)
  2. Refer to the working example in esp-skainet: https://github.com/espressif/esp-skainet/tree/master/examples/usb_mic_recorder

It should help you to achieve the results you want. Meanwhile, we will keep the issue open and add the feature to the plan, but the priority will be not so high.

If there is someone else, who needs the same changes, please do not hesitate to report also.

akumpf commented 11 months ago

Thanks @roma-jam. I totally understand that audio may take some more time/testing to sort it out. Really appreciate you looking into it and providing an alternative. 👍

akumpf commented 10 months ago

I've been continuing to experiment with getting USB audio as a simple 48khz mono/mic input. Even pulling in the example code from other projects, it never shows up for me as an audio interface on the computer. CDC, MSC, MIDI, etc. all work well, but audio does not. 🤷

I've been able to run the other Espressif_TinyUSB examples just fine (via VSCode), but when I modify those to attempt audio/UAC2 (with additional flags, descriptors, etc. as included above), it still doesn't appear to the host as an audio device.

Could the most basic USB device audio example be added, just to illustrate the minimum code/overrides needed to use ESP32 S2/S3 as an audio input?

roma-jam commented 10 months ago

Hi @akumpf,

Which OS do you use? Did you try to check the system info (Device Manager for Win or dmesg for Linux) after plugging your device? Probably there could be an answer.

Right now I would suggested:

  1. When I have made the PR for adding UAC example to esp_tinyusb: https://github.com/espressif/idf-extra-components/pull/282 I checked it and I was able to see the device after connection to Linux host. You could refer to the changes there and try to add them localy. There is a possibility to use idf-extra-component from the local folder (with your local changes). Please refer to the Defining Dependencies in the Manifest
  2. Please, refer the example, I have mentioned before: https://github.com/espressif/esp-skainet/tree/master/examples/usb_mic_recorder. It requires specific dev board but at least it could be a good example of your goal.

Hope these points will help. If there will be more questions, please do not hesitate to ask them.

akumpf commented 10 months ago

Thanks @roma-jam! I was finally able to get the ESP32-S3 sending audio USB (UAC2) using the approach you suggested. It's also providing MIDI and CDC Serial at the same time, so it feels like a pretty useful combination...

I'll work with it a bit more and then hope to report back with an updated code snippet and/or pull request once it's feeling solid. 👏

njreid commented 7 months ago

Hey @akumpf - did you get a chance to make any progress on this? Would love to try any PR you have. It seems like the combination of MIDI, UAC2, CDC serial and maybe even HID would be super useful to a lot of people.

jake-is-ESD-protected commented 5 months ago

Just found this issue a few days after some promising activity on the esp-iot-solutions repo. I was able to get some code working by adding the linked usb_device_uac sub-component manually to my PlatformIO project using the espidf framework and the tinyUSB repo. However, since the feature is pretty new, I had to do some inter-pull-request reverse engineering.

For whatever reason, the official USB Device UAC docs mention this PR, which contains USB Device UAC driver code modifications on tinyUSB's side actually used in the official USB device UAC example by Espressif. Trying to build the project consisting of a tinyUSB dependency in platformio.ini (see below) and the usb_device_uac driver from esp-iot-solutions (as folder under lib) fails for various code mismatches because of the mentioned dependency on a not yet merged PR. I have documented the diffs here:

With those changes manually applied to the PR version of the repo by @HiFiPhile, I was able to compile and flash the following code:

// main.c
#include <stdio.h>
#include <math.h>
#include "esp_err.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2s.h"
#include "tusb.h"
#include "usb_device_uac.h"

static const char *TAG = "usb_uac_main";

#define SAMPLE_RATE 44100
#define BITS_PER_SAMPLE I2S_BITS_PER_SAMPLE_16BIT
#define CHANNEL_FORMAT I2S_CHANNEL_FMT_RIGHT_LEFT

static esp_err_t uac_device_output_cb(uint8_t *buf, size_t len, void *arg)
{
    size_t bytes_written = 0;
    esp_err_t err = i2s_write(0, buf, len, &bytes_written, portMAX_DELAY);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "i2s write failed: %s", esp_err_to_name(err));
        return err;
    }
    ESP_LOGI(TAG, "Output CB triggered, bytes_written: %d", bytes_written);
    return ESP_OK;
}

static esp_err_t uac_device_input_cb(uint8_t *buf, size_t len, size_t *bytes_read, void *arg)
{
    // Not used for output-only mode
    return ESP_OK;
}

static void uac_device_set_mute_cb(uint32_t mute, void *arg)
{
    ESP_LOGI(TAG, "uac_device_set_mute_cb: %"PRIu32"", mute);
}

static void uac_device_set_volume_cb(uint32_t volume, void *arg)
{
    ESP_LOGI(TAG, "uac_device_set_volume_cb: %"PRIu32"", volume);
}

void app_main(void)
{
    i2s_config_t i2s_config = {
        .mode = I2S_MODE_MASTER | I2S_MODE_TX,
        .sample_rate = SAMPLE_RATE,
        .bits_per_sample = BITS_PER_SAMPLE,
        .channel_format = CHANNEL_FORMAT,
        .communication_format = I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB,
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
        .dma_buf_count = 8,
        .dma_buf_len = 64,
        .use_apll = false
    };

    i2s_pin_config_t pin_config = {
        .bck_io_num = 15,
        .ws_io_num = 16,
        .data_out_num = 17
    };

    esp_err_t err = i2s_driver_install(0, &i2s_config, 0, NULL);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "i2s_driver_install failed: %s", esp_err_to_name(err));
        return;
    }

    err = i2s_set_pin(0, &pin_config);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "i2s_set_pin failed: %s", esp_err_to_name(err));
        return;
    }

    uac_device_config_t config = {
        .output_cb = uac_device_output_cb,
        .input_cb = uac_device_input_cb,
        .set_mute_cb = uac_device_set_mute_cb,
        .set_volume_cb = uac_device_set_volume_cb,
        .cb_ctx = NULL,
    };

    err = uac_device_init(&config);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "uac_device_init failed: %s", esp_err_to_name(err));
        return;
    }

    ESP_LOGI(TAG, "USB Audio Class device initialized");

    while (1) {
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}
; platformio.ini

[env:esp32-s3-devkitm-1]
platform = espressif32
board = esp32-s3-devkitm-1
framework = espidf
monitor_speed = 115200

lib_deps = 
    ; https://github.com/hathach/tinyusb ; (not yet usable for this example)

    ; the usb_device_uac driver depends on a not yet merged branch of tinyusb
    ; metioned in the official ESP-IoT-solutions docs: https://docs.espressif.com/projects/espressif-esp-iot-solution/en/latest/usb/usb_device/usb_device_uac.html#usb-device-uac
    ; direct link to PR: https://github.com/hathach/tinyusb/pull/2328
    https://github.com/HiFiPhile/tinyusb.git#rx_fb 
    ; however, the pull above has a different parameters in this macro in usbd.h: 
    ;   - https://github.com/HiFiPhile/tinyusb/blob/67456357c54ee81e8418a9e814df774b1a3dee85/src/device/usbd.h#L437
    ;   - https://github.com/hathach/tinyusb/blob/d10b65ada4be7d5754b3128e80a9b4db72bdb23f/src/device/usbd.h#L437
    ; using this fork's rx_fb branch requires fixes here which aren't in the branch but the upstream master:
    ;   - .pio\libdeps\esp32-s3-devkitm-1\TinyUSB\src\portable\espressif\esp32sx\dcd_esp32sx.c, L34: #include xtensa/xtensa_api.h
    ;   - .pio\libdeps\esp32-s3-devkitm-1\TinyUSB\src\device\usbd.h, L437, L438
    ;       replace:
    ;       #define TUD_AUDIO_DESC_STD_AS_ISO_FB_EP(_ep, _epsize, _interval) \
    ;               TUD_AUDIO_DESC_STD_AS_ISO_FB_EP_LEN, TUSB_DESC_ENDPOINT, _ep, (uint8_t) ((uint8_t)TUSB_XFER_ISOCHRONOUS | (uint8_t)TUSB_ISO_EP_ATT_NO_SYNC | (uint8_t)TUSB_ISO_EP_ATT_EXPLICIT_FB), U16_TO_U8S_LE(_epsize), _interval
    ;       with:
    ;       #define TUD_AUDIO_DESC_STD_AS_ISO_FB_EP(_ep, _interval) \
    ;               TUD_AUDIO_DESC_STD_AS_ISO_FB_EP_LEN, TUSB_DESC_ENDPOINT, _ep, (uint8_t) ((uint8_t)TUSB_XFER_ISOCHRONOUS | (uint8_t)TUSB_ISO_EP_ATT_NO_SYNC | (uint8_t)TUSB_ISO_EP_ATT_EXPLICIT_FB), U16_TO_U8S_LE(4), _interval
    ; all of this won't be necessary once the fork is pulled into tinyUSB

build_flags =
    -D TINYUSB_ENABLED=1
    -D CFG_TUSB_MCU=OPT_MCU_ESP32S3
    -I lib/usb_device_uac/include
    -I lib/usb_device_uac/tusb
    -D CONFIG_USB_UAC_ENABLED=1
    -D CONFIG_UAC_SPEAKER_CHANNEL_NUM=2
    -D CONFIG_UAC_MIC_CHANNEL_NUM=2
    -D CONFIG_UAC_SPK_INTERVAL_MS=10
    -D CONFIG_UAC_MIC_INTERVAL_MS=10
    -D CONFIG_UAC_SPK_NEW_PLAY_INTERVAL=10 
    -D CONFIG_UAC_SAMPLE_RATE=48000

    -D CONFIG_TUSB_VID=0xCafe
    -D CONFIG_TUSB_PID=0x4010
    -D CONFIG_TUSB_MANUFACTURER="\"TinyUSB\""
    -D CONFIG_TUSB_PRODUCT="\"Product\""
    -D CONFIG_TUSB_SERIAL_NUM="\"12345678\""

    ; shouldn't these have fallback values on UAC device driver level?
    -D CONFIG_UAC_TINYUSB_TASK_PRIORITY=1
    -D CONFIG_UAC_MIC_TASK_PRIORITY=1
    -D CONFIG_UAC_SPK_TASK_PRIORITY=1
    -D CONFIG_UAC_TINYUSB_TASK_CORE=1
    -D CONFIG_UAC_MIC_TASK_CORE=0
    -D CONFIG_UAC_SPK_TASK_CORE=0
    -D USB_DEVICE_UAC_VER_MAJOR=1 ; dummy value
    -D USB_DEVICE_UAC_VER_MINOR=1 ; dummy value
    -D USB_DEVICE_UAC_VER_PATCH=1 ; dummy value

    ; everything flagged with -D can also be defined in code

As soon as the PR is merged, these manual hotfixes are no longer needed. Sadly, I was not able to trigger the input and output callbacks. However, the volume set and mute callbacks work; the ESP32-S3 also shows up as audio device, which at least is a step into the right direction. Any idea why the audio callbacks are not triggered?

HiFiPhile commented 5 months ago

@jake-is-ESD-protected Didn't know that my PR has already been integrated to espressif :) I'm not very familiar with espressif's compatibility layer, however you can try "raw" examples like https://github.com/hathach/tinyusb/tree/master/examples/device/audio_4_channel_mic_freertos uac2_speaker_fb doesn't have freertos version yet, but once other examples working it's easy to adapt.

jake-is-ESD-protected commented 5 months ago

@HiFiPhile I mean I can't know for sure if it was yours, but the docs a) mention your PR and b) Espressif's usb_device_uac depends on AUDIO_FEEDBACK_METHOD_FIFO_COUNT, which is not implemented in main of tinyUSB yet, but in your PR. You tell me! Thank you for the link and great contributions to tinyUSB.

lijunru-hub commented 5 months ago

Quickly develop a UAC device https://components.espressif.com/components/espressif/usb_device_uac/versions/0.1.0

orlyprofili commented 3 months ago

Quickly develop a UAC device https://components.espressif.com/components/espressif/usb_device_uac/versions/0.1.0

@HiFiPhile this is neat, does it support asynchronous mode? The ESP32-S3 appears to have some of the elements needed (eg a robust dma implementation in the usb peripheral),but it's unclear to me if it's all there.

The link says "Support for synchronous transfer feedback endpoints.", where feedback endpoints are used in asynchronous mode, maybe it's a typo?

lijunru-hub commented 3 months ago

Quickly develop a UAC device https://components.espressif.com/components/espressif/usb_device_uac/versions/0.1.0

@HiFiPhile this is neat, does it support asynchronous mode? The ESP32-S3 appears to have some of the elements needed (eg a robust dma implementation in the usb peripheral),but it's unclear to me if it's all there.

The link says "Support for synchronous transfer feedback endpoints.", where feedback endpoints are used in asynchronous mode, maybe it's a typo?

It is Asynchronous Mode of Synchronous Transmission.

orlyprofili commented 3 months ago

thanks @lijunru-hub I ported the example to esp32s3 devkit c1 v1.0, and it enumerates I notice the usb jtag debugger no longer works, where I can confirm that it works with a basic espidf example like blink. is this a known issue with usb uac on the ESP32-S3-LCD-EV-Board, maybe because usb full speed bandwidth is taken up by uac?