復元・復旧サービス充実|(有)フロンティア・オンライン

復元・復旧サービス充実|(有)フロンティア・オンライン

【深淵オーディオ】(5)UACエンジン取付:ESP32-S3コアでPro Audio仕様USB DAC 自作(DIY)

‹ 2025/12/30 ›

こんにちは。5回目、今回はDDC入り口側USB(UAC)エンジンの取り付けを行います。


その前にこのプロジェクトの現在完成度の説明をさせてください。^^



今現在、このProAudio仕様USBDACの完成度はWindowsホスト対し100%に達しました。

(iPhoneで99%)


そして、ここでいうProAudio仕様の完成度とは、録音・編集スタジオで使うプロ用DAC機材を超える品質で100%の到達点となります。


つまり、現時点で、もはや「プロ機材のトップオブトップに到達している」と断言できます。


「嘘だろ~」とお思いのあなた、「ありえん」と思うあなた、そんなんあったら「わいでも欲しくなるわ」とおもったあなた、「作りが知れれべばいいや」と思うあなた、「興味さえわかない」あなたでも、音楽にさえ興味があれば、我慢してしばらく記事の続きに付いて来てください。


そんなあなた方を確実に「時の境界が見れる世界に連れて行くことができます」。汗


ということで、今後はこのプロジェクトをPopoDACプロジェクトという呼称で説明していきます。

I2S計測器見直し

では、と・・・、その前にI2S計器の取り付け見直しをしましたので、そちらを先に。^^;

取り付け方に問題はありませんでしたが、チョイスしたタイマータイプを交換しました。汗


ESPTimerからGPTimerへのコンバートとなります。


「監督~、まだ3回すよ~、まだやれますよ、やらしてくださいよ~」

「ESPTくん、君ではダメです」


基本スペック・実働精度はどちらも同程度、本来ならコンバートする必要がありません。

ですが、今回、リアルタム性を追求した上でUAC、I2Sの二つの車輪を動かす2つのエンジンと、コントールエンジン、監視エンジンと計4つのエンジンを二つのCPUコアで適正に回転させるのに、ESPTimerである由縁に少しだけのデメリットがありました。


そう、ESPTimerを採用してしまうと、CORE配置に難が発生します。

つまり、これでは重要機能をストレスなく、安心できる状態で稼働させにくいということです。


GPTimerスタート

では、init_monitor()差し替えコードです。

void init_monitor(void)

{

    esp_err_t ret;

    ESP_LOGI(TAG, "init_monitor enter");


#if (TRANSFER_MODEL & TRANSFER_SYNCLK_PCNT)

(...前回同様)

#endif


    // GPTimer 設定


    gptimer_config_t config = {

        .clk_src = GPTIMER_CLK_SRC_APB,

        .direction = GPTIMER_COUNT_UP,

        .resolution_hz = 1000000, // 1MHz = 1 tick = 1us

        .intr_priority = TASK_GPTIMER_INTR_PRIORITY, // 0=低 - 3=最高

        .flags = {

            .intr_shared = 0,

            .allow_pd = 0,

            .backup_before_sleep = 0,

        }

    };

    ret = gptimer_new_timer(&config, &g_gptimer);

    ESP_LOGI(TAG, "%s to create %s", ret == ESP_OK?"Succeeded":"Failed", "gptimer");


    gptimer_alarm_config_t alarm_config = {

        .reload_count = 0,

        .alarm_count = 1000, // 1000us = 1ms

        .flags.auto_reload_on_alarm = true,

    };

    ret = gptimer_set_alarm_action(g_gptimer, &alarm_config);

    ESP_LOGI(TAG, "%s to set alarm time=%dus", ret == ESP_OK?"Succeeded":"Failed", alarm_config.alarm_count);

    

    gptimer_event_callbacks_t cbs = {

        .on_alarm = monitor_task_gpt_isr,

    };

    ret = gptimer_register_event_callbacks(g_gptimer, &cbs, NULL);

    ESP_LOGI(TAG, "%s to set alarm %s", ret == ESP_OK?"Succeeded":"Failed", "monitor_task_gpt_isr");


    ret = gptimer_enable(g_gptimer);

    ESP_LOGI(TAG, "%s to set enable %s", ret == ESP_OK?"Succeeded":"Failed", "gptimer");

    

    ret = gptimer_start(g_gptimer);

    ESP_LOGI(TAG, "%s to start %s", ret == ESP_OK?"Succeeded":"Failed", "gptimer");

}


ESPTimerからGPTimerの変更取り付けに難はありません。

但し、留意点があります。


そう、GPTimerはハードウェアタイマー(4タイマー)&ISR呼び出しとなります。

ここは、FPUユニット切断区間となります。

つまり、どこまでやるかを限定的にする必要があります。


それとTimerGroup(2タイマー)の時代と違って、CPUコアへの張り付きをユーザ指定はできません。


今回はこの2つの制約を2つ同時に上手に乗り越える手法で進めます。


USB(UAC)ドライバ選定

では、本日のお題、まずはドライバ選定です。


USBドライバは既存のもの、自作と幾つかの方法が選べます。

が、Espressif/IDFですので、TinyUSB一択、軽い気持ちでOKです。


それほどTinyUSBは安心できるドライバ、特別な事情を除いて、改変の余地もありません。

わざわざUAC用に専用自作する必要はないってことです。


ただ・・・、今回、私たちが今回のプロジェクトで選んでいる開発環境を思い出してください。

それは、MS VisualCode PlatformIOであり、Espressif-IDEではありません。


ここにまた、幾つかのトラップ(罠)が仕込まれています。

十分理解して、トラップに足を踏み込まないようにします。


Espressif vs PlatformIO問題

Microsoft vs Appleに続き、またしてもか!

ということで、こちらでも陣営問題が発生してます。^^;


(どんだけ~)


「VisualCode PlatformIO」プロジェクトで手にできるEspressif-IDFは本家の若干廉価版となります。汗

これはframeworkバージョンが近しくても似て非なるものとなります。


今回は廉価版で進みます。


TinyUSB、こちらもこの問題に引きずられます。

Espressif配給のTinyは使えません。

ここでは、「TinyUSB本家」を使います。


これもまた、両者は似て非なるものとなりますので留意されたし、です。汗


(ですが、TinyUSBはHardwareIOのインプリメントが良好だったら、本家のものを使った方がいいです。)

(本家思想が汚されてないので綺麗&美味です。)


USB(UAC)エンジンスタンバイ

じゃ、早速取り付け~始動までをやってしまいます。


usb_init()

void usb_init(void)

{

    // PHY 設定

    usb_phy_config_t phy_conf = {

        .controller = USB_PHY_CTRL_OTG,

        .otg_mode = USB_OTG_MODE_DEVICE,

        .target = USB_PHY_TARGET_INT,

#if CONFIG_TINYUSB_RHPORT_HS

        .otg_speed = USB_PHY_SPEED_HIGH,

#endif

    };


    usb_phy_handle_t phy_hdl;

    esp_err_t ret = usb_new_phy(&phy_conf, &phy_hdl);

    if (ret != ESP_OK) {

        printf("USB PHY init failed\n");

        return;

    }


    tusb_init();


    #if (TRANSFER_MODEL&TRANSFER_FEEDBACK_SOFISR)

    // SOF コールバックの有効化はusb_init()の後

    //tud_sof_cb_enable(true);

    #endif

}


初期化はごく普通の立上げ手法となります。


Start Of Frameの重要性

ただ、ここで語るべき重要なことがひとつあります。

tud_sof_cb_enable(true);をコメントアウト、使わない(使えない)ってことです。


UACを正しく制御する超重要かつ唯一の因子、SOFが使えません!泣


これはTinyUSB本家の思想に繋がっています。

TinyUSBでは、USBがAudio Classを使う場合、つまりConfigureDescriptionを飲み込んだ時、

SOFをFeedbackISRに委ねる仕組みが埋め込まれています。


これを嫌がって、ユーザがSOF制約を解除する必要はありません。


(海外ユーザでいくつかSOF制約解除の改変を当てたがる方を見かけました)


思想に従い、UACはAsync Feedbackのモードチョイスをすればよいだけです。


usb_task()

void usb_task(void *param)

{

    // Task Watchdog から除外

    esp_task_wdt_delete(NULL);


    ESP_LOGI(TAG, "usb_task enter");

 

    while (1) {

        tud_task();   // TinyUSB デバイスタスク

    }

}


ここについて、説明の余地はありませんね。

Watchdog から逃れたい思いで、おまじない入れてます。^^;


whileの中身は王道、ここにvTaskDelay()やほかの処理を決して入れてはいけません。

tud_task()はSOFを厳守してここを廻りたいのです。


「何も足さない」・・・これこそが真実です。


tud_audio_feedback_params_cb()

本日の最重要コードとなります。

void tud_audio_feedback_params_cb(uint8_t func_id, uint8_t alt_itf, audio_feedback_params_t* feedback_param)

{

    (void)func_id;

    (void)alt_itf;


#if (TRANSFER_MODEL&TRANSFER_FEEDBACK_SOFFIFO) == TRANSFER_FEEDBACK_SOFFIFO

    // Set feedback method to fifo counting

    // tusbの自動応答なのでアプリケーション側でこれ以上のフォローは要らない

    feedback_param->method = AUDIO_FEEDBACK_METHOD_FIFO_COUNT;

    feedback_param->sample_freq = g_ddc.uac_quality.sample_rate;

#else

    // Set feedback method to pulse counting for device look

    feedback_param->method = AUDIO_FEEDBACK_METHOD_FREQUENCY_FIXED;

    feedback_param->sample_freq = g_ddc.uac_quality.sample_rate;

    feedback_param->frequency.mclk_freq = g_cnt_ideal.mclk;

#endif


    ESP_LOGI(TAG, "tud_audio_feedback_params_cb %d, sample freq: %"PRIu32"", feedback_param->method, feedback_param->sample_freq);

}


tud_audio_feedback_params_cb()は、TinyUSBのAudio SOF ISRを促す唯一の手がかりとなります。


ここで、一般的には2つの選択肢が生まれます。


ひとつは、「AUDIO_FEEDBACK_METHOD_FIFO_COUNT」。

こちらは、FIFOでFsを伝える。

事実上FEEDBACKをTinyUSBにお願いするといった宣言になります。


ひとつは、「AUDIO_FEEDBACK_METHOD_FREQUENCY_FIXED」。

精密MCLKカウンタをもっているので自前でFEEDBACKを返す。


「だから『tud_audio_feedback_interval_isr』で呼び出してくれ」


といった宣言になります。


ProAudio仕様のUSBDACが欲しいあなた、ProAudio仕様に迫ってみたいあなた、面白そうと感じたあなた、選択肢は決まってますね!!


正解、AUDIO_FEEDBACK_METHOD_FREQUENCY_FIXED宣言をします。


ちなみにAudio ISRの立上げ宣言にもう少し幅があります。

XMOSは、USBホストに対し「AUDIO_FEEDBACK_METHOD_FREQUENCY_FLOAT」という概念で応答するケースもあるようです。


(AUDIO_FEEDBACK_METHOD_FREQUENCY_FLOATは、固定のパルスカウンターを積んでないけど俺に任せてくれ、みたいなやつです)

「どういうこと~?」


面白いですね。


desc_configuration

多くの皆さん、ある程度の知識を備えた方は、ここのみぞ知りたいと思っているでしょう。^^;

その気持ち、わかります。

void MakeConfigurationDescriptor(int32_t samplerate, int8_t channels, int8_t bits, int8_t samplebytes) 

{

#if CONFIG_UAC_VERSION==CONFIG_UAC_VERSION10

  // ---------------- Configuration Descriptor ----------------

  uint8_t desc_configuration[] =

  {

    // Configuration Descriptor

    0x09, TUSB_DESC_CONFIGURATION,              // bLength, bDescriptorType (CONFIGURATION)

#if CFG_TUD_AUDIO_ENABLE_FEEDBACK_EP == 1

    U16_BYTES(0x6D 9),              // wTotalLength(109)

#else

    U16_BYTES(0x6D),              // wTotalLength(109)

#endif


    0x02,                    // bNumInterfaces (AC AS)

    0x01,                    // bConfigurationValue

    0x00,                    // iConfiguration

    0x80,                    // bmAttributes (Bus Powered)

    0x32,                    // MaxPower (100mA)


    // ===== Interface 0: Audio Control =====

    0x09, 0x04,              // bLength, bDescriptorType (INTERFACE)

    0x00,                    // bInterfaceNumber

    0x00,                    // bAlternateSetting

    0x00,                    // bNumEndpoints

    0x01,                    // bInterfaceClass (Audio)

    0x01,                    // bInterfaceSubClass (Audio Control)

    0x00,                    // bInterfaceProtocol

    0x00,                    // iInterface


    // Audio Control Header

    0x09, TUSB_DESC_CS_INTERFACE, 0x01,        // bLength, bDescriptorType=CS_INTERFACE, HEADER

    U16_BYTES(CONFIG_UAC_VERSION),              // bcdADC = 1.00

    U16_BYTES(0x27),              // wTotalLength = 39 bytes

    0x01,                    // bInCollection

    0x01,                    // baInterfaceNr[1] = 1


    // Input Terminal (USB Streaming)

    0x0C, TUSB_DESC_CS_INTERFACE, 0x02,        // bLength, CS_INTERFACE, INPUT_TERMINAL

    UAC_ENTITY_INPUT_TERMINAL,                    // bTerminalID

    U16_BYTES(AUDIO_TERM_TYPE_USB_STREAMING),              // wTerminalType = USB Streaming

    0x00,                    // bAssocTerminal

    channels,                    // bNrChannels = 2

    0x03, 0x00,              // wChannelConfig = Left Right

    0x00,                    // iChannelNames

    0x00,                    // iTerminal


    // Feature Unit (Mute Volume)

    0x09, TUSB_DESC_CS_INTERFACE, 0x06,        // bLength, CS_INTERFACE, FEATURE_UNIT

    UAC_ENTITY_FEATURE_UNIT,                    // bUnitID

    0x01,                    // bSourceID (Input Terminal)

    0x01,                    // bControlSize

    0x03,                    // bmaControls[master] (Mute Volume)

    0x00,                    // bmaControls[channel 0]

    0x00,                    // bmaControls[channel 1]


    // Output Terminal (Speaker)

    0x09, TUSB_DESC_CS_INTERFACE, 0x03,        // bLength, CS_INTERFACE, OUTPUT_TERMINAL

    UAC_ENTITY_OUTPUT_TERMINAL,                    // bTerminalID

    U16_BYTES(AUDIO_TERM_TYPE_OUT_GENERIC_SPEAKER),              // wTerminalType = Speaker

    0x00,                    // bAssocTerminal

    0x02,                    // bSourceID (Feature Unit)

    0x04,                    // iTerminal → "PopoDAC Speaker"


    // ===== Interface 1: Audio Streaming =====

    // Alt0 (no endpoints)

    0x09, TUSB_DESC_INTERFACE,              // bLength, INTERFACE

    0x01,                    // bInterfaceNumber

    0x00,                    // bAlternateSetting

    0x00,                    // bNumEndpoints

    0x01,                    // bInterfaceClass (Audio)

    AUDIO_SUBCLASS_STREAMING,                    // bInterfaceSubClass (Audio Streaming)

    0x00,                    // bInterfaceProtocol

    0x00,                    // iInterface


    // Alt1 (with one OUT endpoint)

    0x09, TUSB_DESC_INTERFACE,              // bLength, INTERFACE

    0x01,                    // bInterfaceNumber

    0x01,                    // bAlternateSetting

    (CFG_TUD_AUDIO_ENABLE_FEEDBACK_EP==1?2:1), // bNumEndpoints(Out only or Out FB)

    0x01,                    // bInterfaceClass (Audio)

    AUDIO_SUBCLASS_STREAMING,                    // bInterfaceSubClass (Audio Streaming)

    0x00,                    // bInterfaceProtocol

    0x00,                    // iInterface


    // AS General

    0x07, TUSB_DESC_CS_INTERFACE, 0x01,               // bLength, CS_INTERFACE, AS_GENERAL

    UAC_ENTITY_INPUT_TERMINAL,    // bTerminalLink (Input Terminal ID=1)

    0x01,                           // bDelay

    0x01, 0x00,                     // wFormatTag = PCM


    // Format Type

    0x0B, TUSB_DESC_CS_INTERFACE, 0x02,        // bLength, CS_INTERFACE, FORMAT_TYPE

    AUDIO20_FORMAT_TYPE_I,   // bFormatType = FORMAT_TYPE_I

    channels,                    // bNrChannels = 2

    samplebytes,                    // bSubframeSize = 3 bytes

    bits,                    // bBitResolution = 24

    0x01,                    // bSamFreqType = 1 (Discrete)

    U24_TO_U8S_LE(samplerate),        // tSamFreq[1] = 96000 Hz


    // Endpoint Descriptor (Isochronous OUT)

    0x09, TUSB_DESC_ENDPOINT,                       // bLength, ENDPOINT

    CFG_TUD_AUDIO_FUNC_1_EP_OUT,      // bEndpointAddress = EP1 OUT

    UAC_EP_OUT_ATTRIBUTE_USE, // bmAttributes = Isochronous, Adaptive, Data

    U16_BYTES((samplerate*channels*samplebytes/1000)), // 576 bytes

    0x01,                    // bInterval = 1

    0x00,                    // bRefresh

#if CFG_TUD_AUDIO_ENABLE_FEEDBACK_EP == 1 // bSynchAddress

    CFG_TUD_AUDIO_FUNC_1_EP_FB,

#else

    0,

#endif


#if CFG_TUD_AUDIO_ENABLE_FEEDBACK_EP == 1

    // Feedback Endpoint Descriptor

    0x09,                   // bLength

    TUSB_DESC_ENDPOINT,                   // bDescriptorType = ENDPOINT

    CFG_TUD_AUDIO_FUNC_1_EP_FB,  // bEndpointAddress = IN, EP2

    UAC_EP_OUT_ATTRIBUTE_FEEEDBACK, // bmAttributes = Isochronous, Sync = Feedback, Usage = Data

    0x03, 0x00,             // wMaxPacketSize = 3 bytes (feedback reports are 3 bytes)

    UAC_EXPLICIT_FB_INTERVAL/*0x01*/,                   // bInterval = 1 (1ms)

    0x00,                   // bRefresh

    0x00,                   // bSyncAddress = 0

#endif


    // Class-Specific Audio Data Endpoint

    0x07, TUSB_DESC_CS_ENDPOINT, 0x01,        // bLength, CS_ENDPOINT, EP_GENERAL

#if CFG_TUD_AUDIO_ENABLE_FEEDBACK_EP == 1

    AUDIO10_CS_AS_ISO_DATA_EP_ATT_NON_MAX_PACKETS_OK,                    // bmAttributes

    AUDIO10_CS_AS_ISO_DATA_EP_LOCK_DELAY_UNIT_MILLISEC,                    // bLockDelayUnits

    U16_BYTES(0)             // wLockDelay

#else

    0x00,

    0x00,

    U16_BYTES(0)             // wLockDelay

#endif

};

#else

(...UAC2の記述子)

#endif


  if (g_desc_configuration != NULL)

    free(g_desc_configuration);


  g_desc_configuration = malloc(sizeof(desc_configuration));

  memcpy (g_desc_configuration, desc_configuration, sizeof(desc_configuration));

}


しかし、これを見て、この段階でモノマネをするだけでは、ProAudio仕様には全く手が届きません。

どうしてかは、この記事の回が進んだあるとき、このプロジェクトPopoDACの思想哲学に直面することで明らかになります。


(楽しみですね)^^


さて、desc_configurationの組立はよく見かける形ですね。

大きな違いは、MakeConfigurationDescriptor関数内に配置しているという点です。


そうなのです、TinyUSB Audio Classの唯一の欠点がここで露呈します。

といっても、これはTinyUSB本家の思想ですので、それを理解した上でユーザサイドで対処するほかありません。


PopoDACプロジェクトでは、Audio Quality Presetの動的変更に対応します。

なので、名目記述の記述子は動的生成となります。


あともう少しパーツ選定があります。

でももう少しでPopoDACのProAudio仕様思想哲学を説明できそうです。