/* * Bubbles -- I2S RX to WiFi TX via 802.11 action frames. * * Captures stereo audio from an external I2S source (32-bit slots, Philips), * packs to 24-bit, transmits as RKY protocol action frames at 1000 pps. * ESP32-S3 is I2S master (drives BCLK, WS). Full-duplex I2S ensures * clock output on pins even in RX-dominant mode. */ #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_system.h" #include "esp_wifi.h" #include "esp_timer.h" #include "esp_log.h" #include "esp_mac.h" #include "nvs_flash.h" #include "esp_netif.h" #include "esp_event.h" #include "driver/i2s_std.h" #define TAG "bubbles" static const uint8_t BCAST[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; static const uint8_t OUI_RKY[] = {0x52, 0x4B, 0x59}; #define CHANNEL 1 #define SAMPLE_RATE 48000 #define SAMPLES_PER_PKT 48 #define NUM_CHANNELS 2 #define BYTES_PER_SAMPLE 6 #define AUDIO_PAYLOAD (SAMPLES_PER_PKT * BYTES_PER_SAMPLE) #define TX_PERIOD_US 1000 #define STATS_INTERVAL_MS 5000 #define PIN_BCLK GPIO_NUM_16 #define PIN_WS GPIO_NUM_15 #define PIN_DIN GPIO_NUM_18 typedef struct __attribute__((packed)) { uint32_t frame_seq; uint8_t sub_index; uint8_t pkt_type; uint16_t payload_len; uint16_t burst_width; } rky_hdr_t; #define MAC_HDR_LEN 24 #define ACTION_HDR_LEN 4 #define RKY_HDR_LEN sizeof(rky_hdr_t) #define FRAME_OVERHEAD (MAC_HDR_LEN + ACTION_HDR_LEN + RKY_HDR_LEN) #define MAX_FRAME_LEN (FRAME_OVERHEAD + AUDIO_PAYLOAD) #define I2S_READ_BYTES (SAMPLES_PER_PKT * NUM_CHANNELS * sizeof(int32_t)) static i2s_chan_handle_t s_tx_handle; static i2s_chan_handle_t s_rx_handle; static uint8_t s_silence[SAMPLES_PER_PKT * NUM_CHANNELS * sizeof(int32_t)]; static void i2s_tx_task(void *arg) { size_t written; memset(s_silence, 0, sizeof(s_silence)); while (1) { i2s_channel_write(s_tx_handle, s_silence, sizeof(s_silence), &written, portMAX_DELAY); } } static void i2s_init(void) { i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG( I2S_NUM_0, I2S_ROLE_MASTER); chan_cfg.dma_desc_num = 6; chan_cfg.dma_frame_num = SAMPLES_PER_PKT; ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &s_tx_handle, &s_rx_handle)); i2s_std_config_t std_cfg = { .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE), .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG( I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO), .gpio_cfg = { .mclk = I2S_GPIO_UNUSED, .bclk = PIN_BCLK, .ws = PIN_WS, .dout = GPIO_NUM_17, .din = PIN_DIN, .invert_flags = { .mclk_inv = false, .bclk_inv = false, .ws_inv = false, }, }, }; std_cfg.slot_cfg.slot_bit_width = I2S_SLOT_BIT_WIDTH_32BIT; std_cfg.slot_cfg.ws_width = 32; ESP_ERROR_CHECK(i2s_channel_init_std_mode(s_tx_handle, &std_cfg)); ESP_ERROR_CHECK(i2s_channel_init_std_mode(s_rx_handle, &std_cfg)); ESP_ERROR_CHECK(i2s_channel_enable(s_tx_handle)); ESP_ERROR_CHECK(i2s_channel_enable(s_rx_handle)); xTaskCreatePinnedToCore(i2s_tx_task, "i2s_tx", 2048, NULL, 10, NULL, 1); } static void i2s_32_to_packed24(const int32_t *in, uint8_t *out, int n_stereo_frames) { int j = 0; for (int i = 0; i < n_stereo_frames * NUM_CHANNELS; i++) { uint32_t s = (uint32_t)in[i]; out[j++] = (s >> 24) & 0xFF; out[j++] = (s >> 16) & 0xFF; out[j++] = (s >> 8) & 0xFF; } } static int build_frame(uint8_t *buf, const uint8_t *src, uint32_t seq, const uint8_t *payload, uint16_t plen) { memset(buf, 0, MAC_HDR_LEN); buf[0] = 0xD0; memcpy(buf + 4, BCAST, 6); memcpy(buf + 10, src, 6); memcpy(buf + 16, src, 6); int off = MAC_HDR_LEN; buf[off++] = 127; buf[off++] = OUI_RKY[0]; buf[off++] = OUI_RKY[1]; buf[off++] = OUI_RKY[2]; rky_hdr_t *hdr = (rky_hdr_t *)(buf + off); hdr->frame_seq = seq; hdr->sub_index = 0; hdr->pkt_type = 0; hdr->payload_len = plen; hdr->burst_width = 1; off += RKY_HDR_LEN; if (payload && plen > 0) { memcpy(buf + off, payload, plen); off += plen; } return off; } static void tx_task(void *arg) { uint8_t mac[6]; esp_read_mac(mac, ESP_MAC_WIFI_STA); ESP_LOGI(TAG, "TX: %dkHz packed-24-bit stereo, %d bytes/pkt, %d pps", SAMPLE_RATE / 1000, AUDIO_PAYLOAD, 1000000 / TX_PERIOD_US); uint8_t frame[MAX_FRAME_LEN]; int32_t i2s_buf[SAMPLES_PER_PKT * NUM_CHANNELS]; uint8_t packed_buf[AUDIO_PAYLOAD]; uint32_t seq = 0; uint32_t ok = 0, err = 0; int64_t last_stats = esp_timer_get_time(); int64_t next_tx = esp_timer_get_time(); while (1) { size_t bytes_read = 0; esp_err_t rret = i2s_channel_read(s_rx_handle, i2s_buf, I2S_READ_BYTES, &bytes_read, pdMS_TO_TICKS(10)); if (rret != ESP_OK || bytes_read < I2S_READ_BYTES) continue; int64_t now = esp_timer_get_time(); if (now < next_tx) continue; next_tx += TX_PERIOD_US; if (now >= next_tx) next_tx = now + TX_PERIOD_US; i2s_32_to_packed24(i2s_buf, packed_buf, SAMPLES_PER_PKT); int flen = build_frame(frame, mac, seq, packed_buf, AUDIO_PAYLOAD); esp_err_t ret = esp_wifi_80211_tx(WIFI_IF_STA, frame, flen, false); if (ret == ESP_OK) ok++; else err++; seq++; if ((esp_timer_get_time() - last_stats) >= (int64_t)STATS_INTERVAL_MS * 1000) { float elapsed = (float)STATS_INTERVAL_MS / 1000.f; ESP_LOGI(TAG, "seq=%lu ok=%lu err=%lu pps=%.0f", (unsigned long)seq, (unsigned long)ok, (unsigned long)err, (float)(ok + err) / elapsed); ok = err = 0; last_stats = esp_timer_get_time(); } } } static void wifi_init(void) { ESP_ERROR_CHECK(nvs_flash_init()); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); wifi_config_t sta_cfg = {0}; ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_cfg)); ESP_ERROR_CHECK(esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N)); ESP_ERROR_CHECK(esp_wifi_start()); ESP_ERROR_CHECK(esp_wifi_set_channel(CHANNEL, WIFI_SECOND_CHAN_NONE)); ESP_ERROR_CHECK(esp_wifi_config_80211_tx_rate(WIFI_IF_STA, WIFI_PHY_RATE_54M)); ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); } void app_main(void) { wifi_init(); i2s_init(); xTaskCreatePinnedToCore(tx_task, "tx", 8192, NULL, configMAX_PRIORITIES - 1, NULL, 0); }