main.c 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. /*
  2. * main.c — application entry point for the ESP32-P4 Dante/AES67 endpoint.
  3. *
  4. * Phase 1 target is AES67 interop. This file is adapted from the example
  5. * application in DatanoiseTV/aes67-esp32p4 (Apache-2.0) — the AES67 logic is
  6. * kept as-is; the hardware pin map is sourced from board_config.h so there is
  7. * a single place to retarget the board (see CLAUDE.md / README.md).
  8. *
  9. * SPDX-FileCopyrightText: 2026 DatanoiseTV (original example)
  10. * SPDX-FileCopyrightText: 2026 Benjamin Harris (adaptation)
  11. * SPDX-License-Identifier: Apache-2.0
  12. *
  13. * Flow: init Ethernet (IP101 RMII PHY), wait for IP, bring up the AES67 stack,
  14. * auto-subscribe to the first SAP-discovered source and create a matching
  15. * source stream, capturing/streaming L24 audio via the ES8311 codec.
  16. */
  17. #include <string.h>
  18. #include "freertos/FreeRTOS.h"
  19. #include "freertos/task.h"
  20. #include "freertos/event_groups.h"
  21. #include "esp_log.h"
  22. #include "esp_err.h"
  23. #include "esp_event.h"
  24. #include "esp_netif.h"
  25. #include "nvs_flash.h"
  26. #include "driver/gpio.h"
  27. #include "esp_eth.h"
  28. #include "esp_eth_mac.h"
  29. #include "esp_eth_phy.h"
  30. #include "aes67.h"
  31. #include "aes67_config.h"
  32. #include "aes67_convert.h"
  33. #include "aes67_session.h"
  34. #include "aes67_rtp.h"
  35. #include "aes67_sap.h"
  36. #include "aes67_audio.h"
  37. #include "aes67_webui.h"
  38. #include "dsps_tone_gen.h"
  39. #include "driver/i2c.h"
  40. #include "freertos/stream_buffer.h"
  41. #include "esp_cache.h"
  42. #include "esp_ldo_regulator.h"
  43. #include "driver/i2s_std.h"
  44. #include "es8311.h"
  45. #include "board_config.h"
  46. /* ---- Map the board pin table (board_config.h) onto the names the AES67
  47. * example code below expects. Edit board_config.h, not these. ---- */
  48. #define ETH_MDC_GPIO BOARD_ETH_MDC_GPIO
  49. #define ETH_MDIO_GPIO BOARD_ETH_MDIO_GPIO
  50. #define ETH_REF_CLK_GPIO BOARD_ETH_REF_CLK_GPIO
  51. #define ETH_PHY_RST_GPIO BOARD_ETH_PHY_RST_GPIO
  52. #define ETH_PHY_ADDR BOARD_ETH_PHY_ADDR
  53. #define I2S_MCLK_GPIO BOARD_I2S_MCLK_GPIO
  54. #define I2S_SCLK_GPIO BOARD_I2S_BCLK_GPIO
  55. #define I2S_LRCK_GPIO BOARD_I2S_LRCLK_GPIO
  56. #define I2S_DOUT_GPIO BOARD_I2S_DOUT_GPIO
  57. #define I2S_DIN_GPIO BOARD_I2S_DIN_GPIO
  58. #define PA_CTRL_GPIO BOARD_PA_ENABLE_GPIO
  59. #define I2C_SCL_GPIO BOARD_I2C_SCL_GPIO
  60. #define I2C_SDA_GPIO BOARD_I2C_SDA_GPIO
  61. #define I2C_PORT I2C_NUM_0
  62. #define I2C_CLK_HZ 100000
  63. #define ES8311_ADDR ES8311_ADDRESS_0 /* 0x18, CE pin low */
  64. /* Session handle for SAP callback */
  65. static aes67_session_handle_t s_session = NULL;
  66. static bool s_sink_added = false;
  67. /* Stream params for the Ethernet RTP hook (defined below, used by SAP callback) */
  68. extern uint8_t s_hook_word_length;
  69. extern uint8_t s_hook_channels;
  70. /* SAP discovery callback: auto-subscribe to discovered remote sources */
  71. static void sap_discovery_cb(bool is_announce,
  72. const aes67_sap_remote_source_t *source,
  73. void *user_data)
  74. {
  75. if (!is_announce || !source || s_sink_added || !s_session) {
  76. return;
  77. }
  78. /* Subscribe to any discovered source (first one wins) */
  79. ESP_LOGI("main", "Auto-subscribing to \"%s\" from SAP", source->name);
  80. uint8_t sink_id = 0;
  81. esp_err_t err = aes67_session_add_sink(s_session, source->name,
  82. source->sdp, &sink_id);
  83. if (err == ESP_OK) {
  84. s_sink_added = true;
  85. /* Update Ethernet hook params from the SDP */
  86. aes67_sink_t sink;
  87. if (aes67_session_get_sink(s_session, sink_id, &sink) == ESP_OK) {
  88. s_hook_word_length = sink.rtp_config.word_length;
  89. s_hook_channels = sink.rtp_config.channels;
  90. ESP_LOGI("main", "Hook: wl=%u ch=%u rate=%lu ptime=%u",
  91. s_hook_word_length, s_hook_channels,
  92. (unsigned long)sink.rtp_config.sample_rate,
  93. sink.rtp_config.packet_time_us);
  94. /* Create a matching source with same codec/channels/ptime */
  95. aes67_session_set_packet_time(s_session, sink.rtp_config.packet_time_us);
  96. uint8_t src_id = 0;
  97. esp_err_t src_err = aes67_session_add_source(
  98. s_session, "ESP32-P4 AES67",
  99. sink.rtp_config.channels, sink.rtp_config.codec, &src_id);
  100. if (src_err == ESP_OK) {
  101. ESP_LOGI("main", "Source matched to sink: %uch %s %uus",
  102. sink.rtp_config.channels,
  103. aes67_codec_to_str(sink.rtp_config.codec),
  104. sink.rtp_config.packet_time_us);
  105. }
  106. }
  107. ESP_LOGI("main", "Sink added (id=%u) -- receiving \"%s\"",
  108. sink_id, source->name);
  109. } else {
  110. ESP_LOGW("main", "Failed to add sink for \"%s\": %s",
  111. source->name, esp_err_to_name(err));
  112. }
  113. }
  114. static const char *TAG = "main";
  115. /* Event group bit set when the Ethernet interface obtains an IP */
  116. #define ETH_GOT_IP_BIT BIT0
  117. static EventGroupHandle_t s_eth_event_group;
  118. /* Ethernet link event handler */
  119. static void eth_event_handler(void *arg, esp_event_base_t event_base,
  120. int32_t event_id, void *event_data)
  121. {
  122. switch (event_id) {
  123. case ETHERNET_EVENT_CONNECTED:
  124. ESP_LOGI(TAG, "ethernet link up");
  125. break;
  126. case ETHERNET_EVENT_DISCONNECTED:
  127. ESP_LOGW(TAG, "ethernet link down");
  128. break;
  129. case ETHERNET_EVENT_START:
  130. ESP_LOGI(TAG, "ethernet started");
  131. break;
  132. case ETHERNET_EVENT_STOP:
  133. ESP_LOGI(TAG, "ethernet stopped");
  134. break;
  135. default:
  136. break;
  137. }
  138. }
  139. /* IP acquisition event handler */
  140. static void ip_event_handler(void *arg, esp_event_base_t event_base,
  141. int32_t event_id, void *event_data)
  142. {
  143. if (event_id == IP_EVENT_ETH_GOT_IP) {
  144. ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
  145. ESP_LOGI(TAG, "got IP: " IPSTR, IP2STR(&event->ip_info.ip));
  146. xEventGroupSetBits(s_eth_event_group, ETH_GOT_IP_BIT);
  147. }
  148. }
  149. /* --- Ethernet RTP hook: intercept RTP frames before lwIP --- */
  150. static esp_err_t (*s_original_input)(esp_eth_handle_t, uint8_t*, uint32_t, void*, void*) = NULL;
  151. static void *s_original_priv = NULL;
  152. static i2s_chan_handle_t s_hook_tx_chan = NULL;
  153. static int32_t *s_hook_tmp32 = NULL;
  154. static int16_t *s_hook_i16 = NULL;
  155. uint32_t s_hook_pkt_count = 0; /* Visible to webui for stats */
  156. uint32_t s_hook_drop_count = 0;
  157. uint32_t s_hook_fwd_count = 0;
  158. uint32_t s_hook_total_frames = 0;
  159. static uint16_t s_hook_last_seq = 0;
  160. static uint32_t s_hook_seq_gaps = 0;
  161. uint32_t s_hook_seq_lost = 0;
  162. StreamBufferHandle_t s_hook_sbuf = NULL; /* Global: shared with playback task */
  163. uint8_t s_hook_word_length = 3; /* Default L24. Updated from SDP. */
  164. uint8_t s_hook_channels = 2; /* Default stereo. Updated from SDP. */
  165. /* Called for EVERY Ethernet frame, before lwIP.
  166. * RTP multicast on port 5004: process + free. Everything else: forward to lwIP. */
  167. static esp_err_t IRAM_ATTR eth_rtp_hook(esp_eth_handle_t eth_handle,
  168. uint8_t *buffer, uint32_t length,
  169. void *priv, void *info)
  170. {
  171. s_hook_total_frames++;
  172. /* Quick reject: minimum Ethernet+IP+UDP+RTP header = 54 bytes */
  173. if (length < 54 || !s_hook_tx_chan) goto forward;
  174. /* EtherType must be IPv4 */
  175. if (buffer[12] != 0x08 || buffer[13] != 0x00) goto forward;
  176. /* IP protocol must be UDP (17) */
  177. uint8_t ihl = (buffer[14] & 0x0F) * 4;
  178. if (buffer[14 + 9] != 17) goto forward;
  179. /* Must be IPv4 multicast destination (224.0.0.0/4) */
  180. if ((buffer[14 + 16] & 0xF0) != 0xE0) goto forward;
  181. /* UDP destination port must be 5004 */
  182. int udp_off = 14 + ihl;
  183. uint16_t dst_port = (buffer[udp_off + 2] << 8) | buffer[udp_off + 3];
  184. if (dst_port != 5004) goto forward;
  185. /* RTP version must be 2 */
  186. int rtp_off = udp_off + 8;
  187. if ((buffer[rtp_off] >> 6) != 2) goto forward;
  188. /* Track RTP sequence numbers to detect packet loss */
  189. {
  190. uint16_t seq = (buffer[rtp_off + 2] << 8) | buffer[rtp_off + 3];
  191. if (s_hook_pkt_count > 0) {
  192. uint16_t expected = s_hook_last_seq + 1;
  193. if (seq != expected) {
  194. int16_t gap = (int16_t)(seq - expected);
  195. if (gap > 0 && gap < 100) {
  196. s_hook_seq_gaps++;
  197. s_hook_seq_lost += gap;
  198. }
  199. }
  200. }
  201. s_hook_last_seq = seq;
  202. }
  203. /* Extract RTP payload */
  204. uint8_t cc = buffer[rtp_off] & 0x0F;
  205. int payload_off = rtp_off + 12 + cc * 4;
  206. /* Handle RTP header extension */
  207. if (buffer[rtp_off] & 0x10) {
  208. if ((int)length < payload_off + 4) goto forward;
  209. uint16_t ext_len = (buffer[payload_off + 2] << 8) | buffer[payload_off + 3];
  210. payload_off += 4 + ext_len * 4;
  211. }
  212. if (payload_off >= (int)length) goto forward;
  213. const uint8_t *payload = buffer + payload_off;
  214. int payload_len = length - payload_off;
  215. /* ESP-IDF Ethernet driver already does M2C cache sync in
  216. * emac_esp_dma_receive_frame(). No additional sync needed. */
  217. /* Handle RTP padding (P bit) */
  218. if (buffer[rtp_off] & 0x20) {
  219. uint8_t pad_len = buffer[length - 1];
  220. payload_len -= pad_len;
  221. if (payload_len <= 0) goto forward;
  222. }
  223. /* Use UDP length field instead of Ethernet frame length to avoid
  224. * including Ethernet FCS or padding bytes in the audio data.
  225. * UDP length = header(8) + payload. */
  226. uint16_t udp_len = (buffer[udp_off + 4] << 8) | buffer[udp_off + 5];
  227. int rtp_total = udp_len - 8; /* UDP payload = RTP header + RTP payload */
  228. int rtp_payload = rtp_total - (payload_off - rtp_off);
  229. if (rtp_payload > 0 && rtp_payload < payload_len) {
  230. payload_len = rtp_payload; /* Trim to actual RTP payload */
  231. }
  232. /* Use word length and channel count from SDP (set when sink is added) */
  233. int wl = s_hook_word_length;
  234. int ch = s_hook_channels;
  235. int frame_bytes = ch * wl;
  236. int frames = payload_len / frame_bytes;
  237. if (frames <= 0 || frames > AES67_MAX_TIC_FRAMES) goto forward;
  238. /* Log first packet details */
  239. if (s_hook_pkt_count == 0) {
  240. ESP_DRAM_LOGI("eth_hook", "FIRST PKT: payload=%d bytes, wl=%d, ch=%d, frames=%d, "
  241. "write_bytes=%d", payload_len, wl, ch, frames,
  242. frames * ch * (int)sizeof(int32_t));
  243. }
  244. /* Convert to int32 -- stereo passthrough, no mono mix */
  245. aes67_convert_from_net(payload, s_hook_tmp32, frames, ch, wl);
  246. /* Write int32 stereo directly to stream buffer (read by DMA ISR) */
  247. extern void aes67_audio_slot_write(const void *data, uint32_t len);
  248. aes67_audio_slot_write(s_hook_tmp32, frames * ch * sizeof(int32_t));
  249. s_hook_pkt_count++;
  250. /* Rate-limited loss reporting: max once per second */
  251. static uint32_t s_last_seq_lost = 0;
  252. static uint32_t s_last_loss_log_pkt = 0;
  253. if (s_hook_seq_lost != s_last_seq_lost &&
  254. (s_hook_pkt_count - s_last_loss_log_pkt) > 8000) {
  255. ESP_LOGW("eth_hook", "SEQ LOSS: %lu gaps, %lu lost (total %lu pkts)",
  256. (unsigned long)s_hook_seq_gaps,
  257. (unsigned long)s_hook_seq_lost,
  258. (unsigned long)s_hook_pkt_count);
  259. s_last_seq_lost = s_hook_seq_lost;
  260. s_last_loss_log_pkt = s_hook_pkt_count;
  261. }
  262. free(buffer); /* We consumed the frame */
  263. return ESP_OK;
  264. forward:
  265. s_hook_fwd_count++;
  266. /* Not RTP -- forward to lwIP via esp_netif_receive */
  267. if (s_original_priv) {
  268. return esp_netif_receive((esp_netif_t *)s_original_priv, buffer, length, NULL);
  269. }
  270. free(buffer);
  271. return ESP_OK;
  272. }
  273. /*
  274. * Initialize the IP101 Ethernet PHY over RMII.
  275. * The ESP32-P4-NANO uses an external 50MHz crystal for the RMII reference
  276. * clock, so we configure EMAC_CLK_EXT_IN on GPIO50.
  277. */
  278. static esp_err_t ethernet_init(esp_eth_handle_t *out_eth_handle)
  279. {
  280. /* EMAC configuration with RMII pin mapping */
  281. eth_esp32_emac_config_t emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG();
  282. emac_config.smi_gpio.mdc_num = ETH_MDC_GPIO;
  283. emac_config.smi_gpio.mdio_num = ETH_MDIO_GPIO;
  284. emac_config.interface = EMAC_DATA_INTERFACE_RMII;
  285. emac_config.clock_config.rmii.clock_mode = EMAC_CLK_EXT_IN;
  286. emac_config.clock_config.rmii.clock_gpio = ETH_REF_CLK_GPIO;
  287. eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
  288. mac_config.flags = ETH_MAC_FLAG_PIN_TO_CORE; /* Pin to core 0 */
  289. mac_config.rx_task_prio = 24; /* Highest: above lwIP (23), feeds audio ISR directly */
  290. esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&emac_config, &mac_config);
  291. if (!mac) {
  292. ESP_LOGE(TAG, "failed to create EMAC");
  293. return ESP_FAIL;
  294. }
  295. /* IP101 PHY at address 1, with hardware reset on GPIO51 */
  296. eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
  297. phy_config.phy_addr = ETH_PHY_ADDR;
  298. phy_config.reset_gpio_num = ETH_PHY_RST_GPIO;
  299. esp_eth_phy_t *phy = esp_eth_phy_new_ip101(&phy_config);
  300. if (!phy) {
  301. ESP_LOGE(TAG, "failed to create IP101 PHY");
  302. return ESP_FAIL;
  303. }
  304. /* Install the Ethernet driver */
  305. esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy);
  306. esp_eth_handle_t eth_handle = NULL;
  307. esp_err_t ret = esp_eth_driver_install(&eth_config, &eth_handle);
  308. if (ret != ESP_OK) {
  309. ESP_LOGE(TAG, "ethernet driver install failed: %s", esp_err_to_name(ret));
  310. return ret;
  311. }
  312. /* Create the netif with key "ETH_0" so the L2 TAP interface used by
  313. * ptpd can find it. The default ESP_NETIF_DEFAULT_ETH uses "ETH_DEF"
  314. * which does not match what ptpd_start("ETH_0") expects. */
  315. esp_netif_inherent_config_t base_cfg = ESP_NETIF_INHERENT_DEFAULT_ETH();
  316. base_cfg.if_key = "ETH_0";
  317. esp_netif_config_t netif_config = {
  318. .base = &base_cfg,
  319. .stack = ESP_NETIF_NETSTACK_DEFAULT_ETH,
  320. };
  321. esp_netif_t *eth_netif = esp_netif_new(&netif_config);
  322. if (!eth_netif) {
  323. ESP_LOGE(TAG, "failed to create ethernet netif");
  324. return ESP_FAIL;
  325. }
  326. /* Attach the driver to the netif via the default glue layer */
  327. esp_eth_netif_glue_handle_t glue = esp_eth_new_netif_glue(eth_handle);
  328. ret = esp_netif_attach(eth_netif, glue);
  329. if (ret != ESP_OK) {
  330. ESP_LOGE(TAG, "failed to attach ethernet to netif: %s", esp_err_to_name(ret));
  331. return ret;
  332. }
  333. /* Register event handlers for link and IP events */
  334. ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID,
  335. &eth_event_handler, NULL));
  336. ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP,
  337. &ip_event_handler, NULL));
  338. /* Ethernet RTP hook: intercept RTP at MAC level before lwIP */
  339. s_original_priv = eth_netif;
  340. /* 192 frames * max channels covers the AES67 practical max (4ms at 48kHz) */
  341. s_hook_tmp32 = heap_caps_malloc(
  342. 192 * AES67_MAX_CHANNELS_PER_STREAM * sizeof(int32_t),
  343. MALLOC_CAP_INTERNAL);
  344. s_hook_i16 = heap_caps_malloc(
  345. 192 * AES67_MAX_CHANNELS_PER_STREAM * sizeof(int16_t),
  346. MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
  347. esp_eth_update_input_path_info(eth_handle, eth_rtp_hook, eth_netif);
  348. ESP_LOGI(TAG, "Ethernet RTP hook installed");
  349. /* Enable Ethernet flow control to prevent frame drops under load.
  350. * Without this, the EMAC drops ~8% of multicast frames at high
  351. * packet rates (1000 pps) because the DMA can't keep up. */
  352. bool flow_ctrl = true;
  353. esp_eth_ioctl(eth_handle, ETH_CMD_S_FLOW_CTRL, &flow_ctrl);
  354. /* Start Ethernet */
  355. ret = esp_eth_start(eth_handle);
  356. if (ret != ESP_OK) {
  357. ESP_LOGE(TAG, "ethernet start failed: %s", esp_err_to_name(ret));
  358. return ret;
  359. }
  360. *out_eth_handle = eth_handle;
  361. return ESP_OK;
  362. }
  363. /* Enable the power amplifier on the ES8311 output stage */
  364. static void pa_ctrl_enable(void)
  365. {
  366. gpio_config_t io_conf = {
  367. .pin_bit_mask = (1ULL << PA_CTRL_GPIO),
  368. .mode = GPIO_MODE_OUTPUT,
  369. .pull_up_en = GPIO_PULLUP_DISABLE,
  370. .pull_down_en = GPIO_PULLDOWN_DISABLE,
  371. .intr_type = GPIO_INTR_DISABLE,
  372. };
  373. gpio_config(&io_conf);
  374. gpio_set_level(PA_CTRL_GPIO, 1);
  375. ESP_LOGI(TAG, "PA enabled (GPIO%d high)", PA_CTRL_GPIO);
  376. }
  377. /*
  378. * Initialize the ES8311 audio codec via I2C.
  379. * Configures it for 48kHz 24-bit I2S operation with both DAC (playback)
  380. * and ADC (capture) enabled.
  381. */
  382. static esp_err_t es8311_codec_init(void)
  383. {
  384. /* Initialize I2C master bus */
  385. i2c_config_t i2c_cfg = {
  386. .mode = I2C_MODE_MASTER,
  387. .sda_io_num = I2C_SDA_GPIO,
  388. .scl_io_num = I2C_SCL_GPIO,
  389. .sda_pullup_en = GPIO_PULLUP_ENABLE,
  390. .scl_pullup_en = GPIO_PULLUP_ENABLE,
  391. .master.clk_speed = I2C_CLK_HZ,
  392. };
  393. esp_err_t ret = i2c_param_config(I2C_PORT, &i2c_cfg);
  394. if (ret != ESP_OK) {
  395. ESP_LOGE(TAG, "I2C param config failed: %s", esp_err_to_name(ret));
  396. return ret;
  397. }
  398. ret = i2c_driver_install(I2C_PORT, I2C_MODE_MASTER, 0, 0, 0);
  399. if (ret != ESP_OK) {
  400. ESP_LOGE(TAG, "I2C driver install failed: %s", esp_err_to_name(ret));
  401. return ret;
  402. }
  403. /* Create ES8311 codec handle */
  404. es8311_handle_t codec = es8311_create(I2C_PORT, ES8311_ADDR);
  405. if (!codec) {
  406. ESP_LOGE(TAG, "ES8311 create failed");
  407. return ESP_FAIL;
  408. }
  409. /* Configure clock: MCLK from MCLK pin, 48kHz sample rate.
  410. * I2S uses MCLK_MULTIPLE_384 (matching official ESP-IDF example).
  411. * MCLK = 48000 * 384 = 18.432 MHz */
  412. es8311_clock_config_t clk_cfg = {
  413. .mclk_inverted = false,
  414. .sclk_inverted = false,
  415. .mclk_from_mclk_pin = true,
  416. .mclk_frequency = 48000 * 384, /* 18.432 MHz */
  417. .sample_frequency = 48000,
  418. };
  419. /* 32-bit resolution -- int32 maps directly, no truncation */
  420. ret = es8311_init(codec, &clk_cfg, ES8311_RESOLUTION_32, ES8311_RESOLUTION_32);
  421. if (ret != ESP_OK) {
  422. ESP_LOGE(TAG, "ES8311 init failed: %s", esp_err_to_name(ret));
  423. return ret;
  424. }
  425. ret = es8311_voice_volume_set(codec, 70, NULL);
  426. if (ret != ESP_OK) {
  427. ESP_LOGW(TAG, "ES8311 volume set failed: %s", esp_err_to_name(ret));
  428. }
  429. /* Unmute DAC output */
  430. ret = es8311_voice_mute(codec, false);
  431. if (ret != ESP_OK) {
  432. ESP_LOGW(TAG, "ES8311 unmute failed: %s", esp_err_to_name(ret));
  433. }
  434. /* Configure microphone: analog mic, 24dB gain */
  435. es8311_microphone_config(codec, false);
  436. es8311_microphone_gain_set(codec, ES8311_MIC_GAIN_24DB);
  437. /* Dump ES8311 register state for debugging */
  438. es8311_register_dump(codec);
  439. ESP_LOGI(TAG, "ES8311 codec initialized (48kHz, 16-bit, MCLK=18.432MHz, vol=100)");
  440. return ESP_OK;
  441. }
  442. void app_main(void)
  443. {
  444. /* Initialize NVS -- required by some ESP-IDF components internally */
  445. esp_err_t ret = nvs_flash_init();
  446. if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
  447. ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
  448. ESP_ERROR_CHECK(nvs_flash_erase());
  449. ret = nvs_flash_init();
  450. }
  451. ESP_ERROR_CHECK(ret);
  452. /* Configure LDO4 to 3.3V for SDIO/SPI power domain.
  453. * Must be done early before any peripherals on this rail. */
  454. esp_ldo_channel_handle_t ldo4 = NULL;
  455. esp_ldo_channel_config_t ldo_cfg = {
  456. .chan_id = 4,
  457. .voltage_mv = 3300,
  458. };
  459. if (esp_ldo_acquire_channel(&ldo_cfg, &ldo4) == ESP_OK) {
  460. ESP_LOGI(TAG, "LDO4 set to 3.3V");
  461. } else {
  462. ESP_LOGW(TAG, "LDO4 config failed (may already be set)");
  463. }
  464. /* Create the default event loop used by Ethernet and AES67 events */
  465. ESP_ERROR_CHECK(esp_event_loop_create_default());
  466. /* Initialize the TCP/IP stack */
  467. ESP_ERROR_CHECK(esp_netif_init());
  468. /* Set up the event group used to synchronize on IP acquisition */
  469. s_eth_event_group = xEventGroupCreate();
  470. /* Bring up Ethernet with the IP101 PHY */
  471. esp_eth_handle_t eth_handle = NULL;
  472. ESP_ERROR_CHECK(ethernet_init(&eth_handle));
  473. /* Block until we have an IP address. The AES67 stack needs network
  474. * connectivity for PTP, RTP multicast and SAP. */
  475. ESP_LOGI(TAG, "waiting for IP address...");
  476. xEventGroupWaitBits(s_eth_event_group, ETH_GOT_IP_BIT,
  477. pdFALSE, pdTRUE, portMAX_DELAY);
  478. /* Enable the power amplifier before starting audio */
  479. pa_ctrl_enable();
  480. /* ES8311 codec init is deferred until AFTER the AES67 I2S driver starts.
  481. * The ES8311 needs MCLK to be running during initialization. */
  482. /* Build the AES67 node configuration with our I2S pins and codec settings */
  483. aes67_config_t aes67_cfg = AES67_CONFIG_DEFAULT();
  484. strlcpy(aes67_cfg.node_name, "ESP32-P4 AES67", sizeof(aes67_cfg.node_name));
  485. aes67_cfg.net.eth_handle = eth_handle;
  486. aes67_cfg.i2s_pins.mck_gpio = I2S_MCLK_GPIO;
  487. aes67_cfg.i2s_pins.bck_gpio = I2S_SCLK_GPIO;
  488. aes67_cfg.i2s_pins.ws_gpio = I2S_LRCK_GPIO;
  489. aes67_cfg.i2s_pins.dout_gpio = I2S_DOUT_GPIO;
  490. aes67_cfg.i2s_pins.din_gpio = I2S_DIN_GPIO;
  491. aes67_cfg.audio.sample_rate = 48000;
  492. aes67_cfg.audio.channels = 2;
  493. aes67_cfg.audio.codec = AES67_CODEC_L24;
  494. aes67_cfg.audio.word_length = 3; /* 24-bit = 3 bytes */
  495. aes67_cfg.audio.packet_time_us = 1000; /* 1ms packet time per AES67 */
  496. /* Initialize the AES67 node */
  497. aes67_node_handle_t node = NULL;
  498. ESP_ERROR_CHECK(aes67_node_init(&aes67_cfg, &node));
  499. /* Create stream buffer BEFORE starting the node. The DMA ISR callback
  500. * reads directly from this buffer, bypassing the playback task.
  501. * 30 packets (~120ms at 4ms ptime) for jitter absorption.
  502. * Trigger=1: ISR always reads whatever is available. */
  503. {
  504. uint32_t pkt_bytes = 192 * aes67_cfg.audio.channels * sizeof(int32_t);
  505. s_hook_sbuf = xStreamBufferCreate(pkt_bytes * 30, 1);
  506. /* Wire the stream buffer to the audio driver's DMA ISR */
  507. aes67_audio_handle_t audio = NULL;
  508. aes67_node_get_audio(node, &audio);
  509. aes67_audio_set_isr_stream_buf(audio, s_hook_sbuf);
  510. /* Also register it as the node's playout source for the (non-ISR)
  511. * playback-task fallback path. */
  512. aes67_node_set_playout_buf(node, s_hook_sbuf);
  513. /* Enable I2S RX capture for source (TX) streams */
  514. aes67_audio_init_capture(audio);
  515. s_hook_tx_chan = aes67_audio_get_tx_chan(audio);
  516. ESP_LOGI(TAG, "Ethernet hook -> stream buffer -> DMA ISR (zero-overhead)");
  517. ESP_LOGI(TAG, "I2S RX capture -> stream buffer -> RTP TX (ISR-driven)");
  518. }
  519. /* Start PTP sync, RTP engine, SAP announcements and audio I/O.
  520. * This enables I2S which starts MCLK generation. The DMA ISR
  521. * callback is registered during audio start. */
  522. ESP_ERROR_CHECK(aes67_node_start(node));
  523. /* Initialize ES8311 codec AFTER I2S is running (MCLK must be active).
  524. * This is the critical init order discovered through testing. */
  525. esp_err_t codec_ret = es8311_codec_init();
  526. if (codec_ret != ESP_OK) {
  527. ESP_LOGE(TAG, "ES8311 codec init FAILED: %s", esp_err_to_name(codec_ret));
  528. } else {
  529. ESP_LOGI(TAG, "ES8311 codec initialized (MCLK running)");
  530. }
  531. /* Obtain the session manager handle from the node so we can add streams */
  532. aes67_session_handle_t session = NULL;
  533. ESP_ERROR_CHECK(aes67_node_get_session(node, &session));
  534. s_session = session;
  535. /* Register SAP callback to auto-subscribe to discovered sources.
  536. * This will add a sink for the first remote source found (e.g. SIENNA). */
  537. aes67_sap_handle_t sap = NULL;
  538. /* Get SAP handle from the node config - it's stored internally.
  539. * For now, register via the session manager's SAP handle. */
  540. if (aes67_node_get_sap(node, &sap) == ESP_OK && sap) {
  541. aes67_sap_register_cb(sap, sap_discovery_cb, NULL);
  542. ESP_LOGI(TAG, "SAP auto-subscribe enabled");
  543. /* Check if any sources were already discovered before callback
  544. * was registered (SAP starts during node_start). */
  545. int remote_count = aes67_sap_get_remote_count(sap);
  546. for (int i = 0; i < remote_count && !s_sink_added; i++) {
  547. aes67_sap_remote_source_t remote;
  548. if (aes67_sap_get_remote(sap, i, &remote) == ESP_OK) {
  549. sap_discovery_cb(true, &remote, NULL);
  550. }
  551. }
  552. }
  553. /* Source stream is created in the SAP callback to match the
  554. * discovered sink's codec and channel count. */
  555. /* Start the WebUI (HTTP + WebSocket) on port 80 */
  556. aes67_webui_handle_t webui = NULL;
  557. if (aes67_webui_init(node, 80, &webui) == ESP_OK) {
  558. aes67_webui_start(webui);
  559. }
  560. ESP_LOGI(TAG, "node ready");
  561. /* Monitor loop: ISR health only */
  562. extern void aes67_audio_log_isr_stats(void);
  563. while (1) {
  564. vTaskDelay(pdMS_TO_TICKS(2000));
  565. aes67_audio_log_isr_stats();
  566. }
  567. }