From 6792ff6d58e62ba540cf7e92272d6bb1493ee5d8 Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Thu, 24 Apr 2025 22:33:58 +0200 Subject: [PATCH] [i2s_audio, i2s_audio_microphone, i2s_audio_speaker] Add basic support for new esp-idf 5.x.x i2s driver. (#8181) --- esphome/components/i2s_audio/__init__.py | 135 ++++++++++++--- esphome/components/i2s_audio/i2s_audio.h | 46 ++++- .../i2s_audio/media_player/__init__.py | 9 + .../i2s_audio/microphone/__init__.py | 19 +++ .../microphone/i2s_audio_microphone.cpp | 161 +++++++++++++++++- .../microphone/i2s_audio_microphone.h | 13 +- .../components/i2s_audio/speaker/__init__.py | 34 +++- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 145 +++++++++++++++- .../i2s_audio/speaker/i2s_audio_speaker.h | 21 ++- esphome/core/defines.h | 1 + tests/components/micro_wake_word/common.yaml | 1 + .../components/microphone/test.esp32-idf.yaml | 11 +- 12 files changed, 552 insertions(+), 44 deletions(-) diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index fa515a585f..291ae4ba95 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -8,7 +8,15 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S3, ) import esphome.config_validation as cv -from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE +from esphome.const import ( + CONF_BITS_PER_SAMPLE, + CONF_CHANNEL, + CONF_ID, + CONF_SAMPLE_RATE, + KEY_CORE, + KEY_FRAMEWORK_VERSION, +) +from esphome.core import CORE from esphome.cpp_generator import MockObjClass import esphome.final_validate as fv @@ -35,6 +43,9 @@ CONF_MONO = "mono" CONF_LEFT = "left" CONF_RIGHT = "right" CONF_STEREO = "stereo" +CONF_BOTH = "both" + +CONF_USE_LEGACY = "use_legacy" i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio") I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component) @@ -50,6 +61,12 @@ I2S_MODE_OPTIONS = { CONF_SECONDARY: i2s_mode_t.I2S_MODE_SLAVE, # NOLINT } +i2s_role_t = cg.global_ns.enum("i2s_role_t") +I2S_ROLE_OPTIONS = { + CONF_PRIMARY: i2s_role_t.I2S_ROLE_MASTER, # NOLINT + CONF_SECONDARY: i2s_role_t.I2S_ROLE_SLAVE, # NOLINT +} + # https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h I2S_PORTS = { VARIANT_ESP32: 2, @@ -60,10 +77,23 @@ I2S_PORTS = { i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t") I2S_CHANNELS = { - CONF_MONO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ALL_LEFT, - CONF_LEFT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT, - CONF_RIGHT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT, - CONF_STEREO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_RIGHT_LEFT, + CONF_MONO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ALL_LEFT, # left data to both channels + CONF_LEFT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT, # mono data + CONF_RIGHT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT, # mono data + CONF_STEREO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_RIGHT_LEFT, # stereo data to both channels +} + +i2s_slot_mode_t = cg.global_ns.enum("i2s_slot_mode_t") +I2S_SLOT_MODE = { + CONF_MONO: i2s_slot_mode_t.I2S_SLOT_MODE_MONO, + CONF_STEREO: i2s_slot_mode_t.I2S_SLOT_MODE_STEREO, +} + +i2s_std_slot_mask_t = cg.global_ns.enum("i2s_std_slot_mask_t") +I2S_STD_SLOT_MASK = { + CONF_LEFT: i2s_std_slot_mask_t.I2S_STD_SLOT_LEFT, + CONF_RIGHT: i2s_std_slot_mask_t.I2S_STD_SLOT_RIGHT, + CONF_BOTH: i2s_std_slot_mask_t.I2S_STD_SLOT_BOTH, } i2s_bits_per_sample_t = cg.global_ns.enum("i2s_bits_per_sample_t") @@ -83,8 +113,19 @@ I2S_BITS_PER_CHANNEL = { 32: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_32BIT, } +i2s_slot_bit_width_t = cg.global_ns.enum("i2s_slot_bit_width_t") +I2S_SLOT_BIT_WIDTH = { + "default": i2s_slot_bit_width_t.I2S_SLOT_BIT_WIDTH_AUTO, + 8: i2s_slot_bit_width_t.I2S_SLOT_BIT_WIDTH_8BIT, + 16: i2s_slot_bit_width_t.I2S_SLOT_BIT_WIDTH_16BIT, + 24: i2s_slot_bit_width_t.I2S_SLOT_BIT_WIDTH_24BIT, + 32: i2s_slot_bit_width_t.I2S_SLOT_BIT_WIDTH_32BIT, +} + _validate_bits = cv.float_with_unit("bits", "bit") +_use_legacy_driver = None + def i2s_audio_component_schema( class_: MockObjClass, @@ -97,20 +138,22 @@ def i2s_audio_component_schema( { cv.GenerateID(): cv.declare_id(class_), cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), - cv.Optional(CONF_CHANNEL, default=default_channel): cv.enum(I2S_CHANNELS), + cv.Optional(CONF_CHANNEL, default=default_channel): cv.one_of( + *I2S_CHANNELS + ), cv.Optional(CONF_SAMPLE_RATE, default=default_sample_rate): cv.int_range( min=1 ), cv.Optional(CONF_BITS_PER_SAMPLE, default=default_bits_per_sample): cv.All( - _validate_bits, cv.enum(I2S_BITS_PER_SAMPLE) + _validate_bits, cv.one_of(*I2S_BITS_PER_SAMPLE) ), - cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.enum( - I2S_MODE_OPTIONS, lower=True + cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.one_of( + *I2S_MODE_OPTIONS, lower=True ), cv.Optional(CONF_USE_APLL, default=False): cv.boolean, cv.Optional(CONF_BITS_PER_CHANNEL, default="default"): cv.All( cv.Any(cv.float_with_unit("bits", "bit"), "default"), - cv.enum(I2S_BITS_PER_CHANNEL), + cv.one_of(*I2S_BITS_PER_CHANNEL), ), } ) @@ -118,22 +161,60 @@ def i2s_audio_component_schema( async def register_i2s_audio_component(var, config): await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) - - cg.add(var.set_i2s_mode(config[CONF_I2S_MODE])) - cg.add(var.set_channel(config[CONF_CHANNEL])) + if use_legacy(): + cg.add(var.set_i2s_mode(I2S_MODE_OPTIONS[config[CONF_I2S_MODE]])) + cg.add(var.set_channel(I2S_CHANNELS[config[CONF_CHANNEL]])) + cg.add( + var.set_bits_per_sample(I2S_BITS_PER_SAMPLE[config[CONF_BITS_PER_SAMPLE]]) + ) + cg.add( + var.set_bits_per_channel( + I2S_BITS_PER_CHANNEL[config[CONF_BITS_PER_CHANNEL]] + ) + ) + else: + cg.add(var.set_i2s_role(I2S_ROLE_OPTIONS[config[CONF_I2S_MODE]])) + slot_mode = config[CONF_CHANNEL] + if slot_mode != CONF_STEREO: + slot_mode = CONF_MONO + slot_mask = config[CONF_CHANNEL] + if slot_mask not in [CONF_LEFT, CONF_RIGHT]: + slot_mask = CONF_BOTH + cg.add(var.set_slot_mode(I2S_SLOT_MODE[slot_mode])) + cg.add(var.set_std_slot_mask(I2S_STD_SLOT_MASK[slot_mask])) + cg.add( + var.set_slot_bit_width(I2S_SLOT_BIT_WIDTH[config[CONF_BITS_PER_CHANNEL]]) + ) cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) - cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) - cg.add(var.set_bits_per_channel(config[CONF_BITS_PER_CHANNEL])) cg.add(var.set_use_apll(config[CONF_USE_APLL])) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(I2SAudioComponent), - cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number, - } +def validate_use_legacy(value): + global _use_legacy_driver # noqa: PLW0603 + if CONF_USE_LEGACY in value: + if (_use_legacy_driver is not None) and ( + _use_legacy_driver != value[CONF_USE_LEGACY] + ): + raise cv.Invalid( + f"All i2s_audio components must set {CONF_USE_LEGACY} to the same value." + ) + if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino): + raise cv.Invalid("Arduino supports only the legacy i2s driver.") + _use_legacy_driver = value[CONF_USE_LEGACY] + return value + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(I2SAudioComponent), + cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_USE_LEGACY): cv.boolean, + }, + ), + validate_use_legacy, ) @@ -148,12 +229,22 @@ def _final_validate(_): ) +def use_legacy(): + framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + if CORE.using_esp_idf and framework_version >= cv.Version(5, 0, 0): + if not _use_legacy_driver: + return False + return True + + FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + if use_legacy(): + cg.add_define("USE_I2S_LEGACY") cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) if CONF_I2S_BCLK_PIN in config: diff --git a/esphome/components/i2s_audio/i2s_audio.h b/esphome/components/i2s_audio/i2s_audio.h index 7e2798c33d..d8050665e9 100644 --- a/esphome/components/i2s_audio/i2s_audio.h +++ b/esphome/components/i2s_audio/i2s_audio.h @@ -2,9 +2,14 @@ #ifdef USE_ESP32 -#include #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/defines.h" +#ifdef USE_I2S_LEGACY +#include +#else +#include +#endif namespace esphome { namespace i2s_audio { @@ -13,19 +18,33 @@ class I2SAudioComponent; class I2SAudioBase : public Parented { public: +#ifdef USE_I2S_LEGACY void set_i2s_mode(i2s_mode_t mode) { this->i2s_mode_ = mode; } void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; } - void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } void set_bits_per_channel(i2s_bits_per_chan_t bits_per_channel) { this->bits_per_channel_ = bits_per_channel; } +#else + void set_i2s_role(i2s_role_t role) { this->i2s_role_ = role; } + void set_slot_mode(i2s_slot_mode_t slot_mode) { this->slot_mode_ = slot_mode; } + void set_std_slot_mask(i2s_std_slot_mask_t std_slot_mask) { this->std_slot_mask_ = std_slot_mask; } + void set_slot_bit_width(i2s_slot_bit_width_t slot_bit_width) { this->slot_bit_width_ = slot_bit_width; } +#endif + void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; } protected: +#ifdef USE_I2S_LEGACY i2s_mode_t i2s_mode_{}; i2s_channel_fmt_t channel_; - uint32_t sample_rate_; i2s_bits_per_sample_t bits_per_sample_; i2s_bits_per_chan_t bits_per_channel_; +#else + i2s_role_t i2s_role_{}; + i2s_slot_mode_t slot_mode_; + i2s_std_slot_mask_t std_slot_mask_; + i2s_slot_bit_width_t slot_bit_width_; +#endif + uint32_t sample_rate_; bool use_apll_; }; @@ -37,6 +56,7 @@ class I2SAudioComponent : public Component { public: void setup() override; +#ifdef USE_I2S_LEGACY i2s_pin_config_t get_pin_config() const { return { .mck_io_num = this->mclk_pin_, @@ -46,6 +66,20 @@ class I2SAudioComponent : public Component { .data_in_num = I2S_PIN_NO_CHANGE, }; } +#else + i2s_std_gpio_config_t get_pin_config() const { + return {.mclk = (gpio_num_t) this->mclk_pin_, + .bclk = (gpio_num_t) this->bclk_pin_, + .ws = (gpio_num_t) this->lrclk_pin_, + .dout = I2S_GPIO_UNUSED, // add local ports + .din = I2S_GPIO_UNUSED, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false, + }}; + } +#endif void set_mclk_pin(int pin) { this->mclk_pin_ = pin; } void set_bclk_pin(int pin) { this->bclk_pin_ = pin; } @@ -62,9 +96,13 @@ class I2SAudioComponent : public Component { I2SAudioIn *audio_in_{nullptr}; I2SAudioOut *audio_out_{nullptr}; - +#ifdef USE_I2S_LEGACY int mclk_pin_{I2S_PIN_NO_CHANGE}; int bclk_pin_{I2S_PIN_NO_CHANGE}; +#else + int mclk_pin_{I2S_GPIO_UNUSED}; + int bclk_pin_{I2S_GPIO_UNUSED}; +#endif int lrclk_pin_; i2s_port_t port_{}; }; diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index 2882729b1e..bed25b011f 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -14,6 +14,7 @@ from .. import ( I2SAudioComponent, I2SAudioOut, i2s_audio_ns, + use_legacy, ) CODEOWNERS = ["@jesserockz"] @@ -87,6 +88,14 @@ CONFIG_SCHEMA = cv.All( ) +def _final_validate(_): + if not use_legacy(): + raise cv.Invalid("I2S media player is only compatible with legacy i2s driver.") + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 161046e962..4950a25751 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -6,12 +6,15 @@ import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_NUMBER from .. import ( + CONF_CHANNEL, CONF_I2S_DIN_PIN, + CONF_MONO, CONF_RIGHT, I2SAudioIn, i2s_audio_component_schema, i2s_audio_ns, register_i2s_audio_component, + use_legacy, ) CODEOWNERS = ["@jesserockz"] @@ -43,6 +46,12 @@ def validate_esp32_variant(config): raise NotImplementedError +def validate_channel(config): + if config[CONF_CHANNEL] == CONF_MONO: + raise cv.Invalid(f"I2S microphone does not support {CONF_MONO}.") + return config + + BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( i2s_audio_component_schema( I2SAudioMicrophone, @@ -71,9 +80,19 @@ CONFIG_SCHEMA = cv.All( key=CONF_ADC_TYPE, ), validate_esp32_variant, + validate_channel, ) +def _final_validate(config): + if not use_legacy(): + if config[CONF_ADC_TYPE] == "internal": + raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver.") + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 4dbc9dcdac..ef375954cd 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -2,7 +2,12 @@ #ifdef USE_ESP32 +#ifdef USE_I2S_LEGACY #include +#else +#include +#include +#endif #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -16,6 +21,7 @@ static const char *const TAG = "i2s_audio.microphone"; void I2SAudioMicrophone::setup() { ESP_LOGCONFIG(TAG, "Setting up I2S Audio Microphone..."); +#ifdef USE_I2S_LEGACY #if SOC_I2S_SUPPORTS_ADC if (this->adc_) { if (this->parent_->get_port() != I2S_NUM_0) { @@ -24,6 +30,7 @@ void I2SAudioMicrophone::setup() { return; } } else +#endif #endif { if (this->pdm_) { @@ -47,6 +54,9 @@ void I2SAudioMicrophone::start_() { if (!this->parent_->try_lock()) { return; // Waiting for another i2s to return lock } + esp_err_t err; + +#ifdef USE_I2S_LEGACY i2s_driver_config_t config = { .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_RX), .sample_rate = this->sample_rate_, @@ -63,8 +73,6 @@ void I2SAudioMicrophone::start_() { .bits_per_chan = this->bits_per_channel_, }; - esp_err_t err; - #if SOC_I2S_SUPPORTS_ADC if (this->adc_) { config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN); @@ -111,6 +119,109 @@ void I2SAudioMicrophone::start_() { return; } } +#else + i2s_chan_config_t chan_cfg = { + .id = this->parent_->get_port(), + .role = this->i2s_role_, + .dma_desc_num = 4, + .dma_frame_num = 256, + .auto_clear = false, + }; + /* Allocate a new RX channel and get the handle of this channel */ + err = i2s_new_channel(&chan_cfg, NULL, &this->rx_handle_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error creating new I2S channel: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } + + i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT; +#ifdef I2S_CLK_SRC_APLL + if (this->use_apll_) { + clk_src = I2S_CLK_SRC_APLL; + } +#endif + i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config(); +#if SOC_I2S_SUPPORTS_PDM_RX + if (this->pdm_) { + i2s_pdm_rx_clk_config_t clk_cfg = { + .sample_rate_hz = this->sample_rate_, + .clk_src = clk_src, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + .dn_sample_mode = I2S_PDM_DSR_8S, + }; + + i2s_pdm_rx_slot_config_t slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, this->slot_mode_); + switch (this->std_slot_mask_) { + case I2S_STD_SLOT_LEFT: + slot_cfg.slot_mask = I2S_PDM_SLOT_LEFT; + break; + case I2S_STD_SLOT_RIGHT: + slot_cfg.slot_mask = I2S_PDM_SLOT_RIGHT; + break; + case I2S_STD_SLOT_BOTH: + slot_cfg.slot_mask = I2S_PDM_SLOT_BOTH; + break; + } + + /* Init the channel into PDM RX mode */ + i2s_pdm_rx_config_t pdm_rx_cfg = { + .clk_cfg = clk_cfg, + .slot_cfg = slot_cfg, + .gpio_cfg = + { + .clk = pin_config.ws, + .din = this->din_pin_, + .invert_flags = + { + .clk_inv = pin_config.invert_flags.ws_inv, + }, + }, + }; + err = i2s_channel_init_pdm_rx_mode(this->rx_handle_, &pdm_rx_cfg); + } else +#endif + { + i2s_std_clk_config_t clk_cfg = { + .sample_rate_hz = this->sample_rate_, + .clk_src = clk_src, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + }; + i2s_data_bit_width_t data_bit_width; + if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_8BIT) { + data_bit_width = I2S_DATA_BIT_WIDTH_16BIT; + } else { + data_bit_width = I2S_DATA_BIT_WIDTH_8BIT; + } + i2s_std_slot_config_t std_slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(data_bit_width, this->slot_mode_); + std_slot_cfg.slot_bit_width = this->slot_bit_width_; + std_slot_cfg.slot_mask = this->std_slot_mask_; + + pin_config.din = this->din_pin_; + + i2s_std_config_t std_cfg = { + .clk_cfg = clk_cfg, + .slot_cfg = std_slot_cfg, + .gpio_cfg = pin_config, + }; + /* Initialize the channel */ + err = i2s_channel_init_std_mode(this->rx_handle_, &std_cfg); + } + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error initializing I2S channel: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } + + /* Before reading data, start the RX channel first */ + i2s_channel_enable(this->rx_handle_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error enabling I2S Microphone: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } +#endif + this->state_ = microphone::STATE_RUNNING; this->high_freq_.start(); this->status_clear_error(); @@ -128,6 +239,7 @@ void I2SAudioMicrophone::stop() { void I2SAudioMicrophone::stop_() { esp_err_t err; +#ifdef USE_I2S_LEGACY #if SOC_I2S_SUPPORTS_ADC if (this->adc_) { err = i2s_adc_disable(this->parent_->get_port()); @@ -150,6 +262,22 @@ void I2SAudioMicrophone::stop_() { this->status_set_error(); return; } +#else + /* Have to stop the channel before deleting it */ + err = i2s_channel_disable(this->rx_handle_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error stopping I2S microphone: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } + /* If the handle is not needed any more, delete it to release the channel resources */ + err = i2s_del_channel(this->rx_handle_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error deleting I2S channel: %s", esp_err_to_name(err)); + this->status_set_error(); + return; + } +#endif this->parent_->unlock(); this->state_ = microphone::STATE_STOPPED; this->high_freq_.stop(); @@ -158,7 +286,11 @@ void I2SAudioMicrophone::stop_() { size_t I2SAudioMicrophone::read(int16_t *buf, size_t len) { size_t bytes_read = 0; +#ifdef USE_I2S_LEGACY esp_err_t err = i2s_read(this->parent_->get_port(), buf, len, &bytes_read, (100 / portTICK_PERIOD_MS)); +#else + esp_err_t err = i2s_channel_read(this->rx_handle_, buf, len, &bytes_read, (100 / portTICK_PERIOD_MS)); +#endif if (err != ESP_OK) { ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err)); this->status_set_warning(); @@ -171,6 +303,7 @@ size_t I2SAudioMicrophone::read(int16_t *buf, size_t len) { this->status_clear_warning(); // ESP-IDF I2S implementation right-extends 8-bit data to 16 bits, // and 24-bit data to 32 bits. +#ifdef USE_I2S_LEGACY switch (this->bits_per_sample_) { case I2S_BITS_PER_SAMPLE_8BIT: case I2S_BITS_PER_SAMPLE_16BIT: @@ -188,6 +321,30 @@ size_t I2SAudioMicrophone::read(int16_t *buf, size_t len) { ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_); return 0; } +#else +#ifndef USE_ESP32_VARIANT_ESP32 + // For newer ESP32 variants 8 bit data needs to be extended to 16 bit. + if (this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_8BIT) { + size_t samples_read = bytes_read / sizeof(int8_t); + for (size_t i = samples_read - 1; i >= 0; i--) { + int16_t temp = static_cast(reinterpret_cast(buf)[i]) << 8; + buf[i] = temp; + } + return samples_read * sizeof(int16_t); + } +#else + // For ESP32 8/16 bit standard mono mode samples need to be switched. + if (this->slot_mode_ == I2S_SLOT_MODE_MONO && this->slot_bit_width_ <= 16 && !this->pdm_) { + size_t samples_read = bytes_read / sizeof(int16_t); + for (int i = 0; i < samples_read; i += 2) { + int16_t tmp = buf[i]; + buf[i] = buf[i + 1]; + buf[i + 1] = tmp; + } + } +#endif + return bytes_read; +#endif } void I2SAudioMicrophone::read_() { diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index ea3f357624..2ff46fabab 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -17,17 +17,23 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub void stop() override; void loop() override; - +#ifdef USE_I2S_LEGACY void set_din_pin(int8_t pin) { this->din_pin_ = pin; } +#else + void set_din_pin(int8_t pin) { this->din_pin_ = (gpio_num_t) pin; } +#endif + void set_pdm(bool pdm) { this->pdm_ = pdm; } size_t read(int16_t *buf, size_t len) override; +#ifdef USE_I2S_LEGACY #if SOC_I2S_SUPPORTS_ADC void set_adc_channel(adc1_channel_t channel) { this->adc_channel_ = channel; this->adc_ = true; } +#endif #endif protected: @@ -35,10 +41,15 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub void stop_(); void read_(); +#ifdef USE_I2S_LEGACY int8_t din_pin_{I2S_PIN_NO_CHANGE}; #if SOC_I2S_SUPPORTS_ADC adc1_channel_t adc_channel_{ADC1_CHANNEL_MAX}; bool adc_{false}; +#endif +#else + gpio_num_t din_pin_{I2S_GPIO_UNUSED}; + i2s_chan_handle_t rx_handle_; #endif bool pdm_{false}; diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index aa3b50d336..7e41cd3991 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -26,6 +26,7 @@ from .. import ( i2s_audio_component_schema, i2s_audio_ns, register_i2s_audio_component, + use_legacy, ) AUTO_LOAD = ["audio"] @@ -60,7 +61,7 @@ I2C_COMM_FMT_OPTIONS = { "pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_LONG, } -NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] +INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32] def _set_num_channels_from_config(config): @@ -101,7 +102,7 @@ def _validate_esp32_variant(config): if config[CONF_DAC_TYPE] != "internal": return config variant = esp32.get_esp32_variant() - if variant in NO_INTERNAL_DAC_VARIANTS: + if variant not in INTERNAL_DAC_VARIANTS: raise cv.Invalid(f"{variant} does not have an internal DAC") return config @@ -143,8 +144,8 @@ CONFIG_SCHEMA = cv.All( cv.Required( CONF_I2S_DOUT_PIN ): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_I2S_COMM_FMT, default="stand_i2s"): cv.enum( - I2C_COMM_FMT_OPTIONS, lower=True + cv.Optional(CONF_I2S_COMM_FMT, default="stand_i2s"): cv.one_of( + *I2C_COMM_FMT_OPTIONS, lower=True ), } ), @@ -157,6 +158,19 @@ CONFIG_SCHEMA = cv.All( ) +def _final_validate(config): + if not use_legacy(): + if config[CONF_DAC_TYPE] == "internal": + raise cv.Invalid("Internal DAC is only compatible with legacy i2s driver.") + if config[CONF_I2S_COMM_FMT] == "stand_max": + raise cv.Invalid( + "I2S standard max format only implemented with legacy i2s driver." + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -167,7 +181,17 @@ async def to_code(config): cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL])) else: cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) - cg.add(var.set_i2s_comm_fmt(config[CONF_I2S_COMM_FMT])) + if use_legacy(): + cg.add( + var.set_i2s_comm_fmt(I2C_COMM_FMT_OPTIONS[config[CONF_I2S_COMM_FMT]]) + ) + else: + fmt = "std" # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long + if config[CONF_I2S_COMM_FMT] in ["stand_msb", "i2s_lsb"]: + fmt = "msb" + elif config[CONF_I2S_COMM_FMT] in ["stand_pcm_short", "pcm_short", "pcm"]: + fmt = "pcm" + cg.add(var.set_i2s_comm_fmt(fmt)) if config[CONF_TIMEOUT] != CONF_NEVER: cg.add(var.set_timeout(config[CONF_TIMEOUT])) cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION])) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index da25914c87..cb3bbc8cf2 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -2,7 +2,11 @@ #ifdef USE_ESP32 +#ifdef USE_I2S_LEGACY #include +#else +#include +#endif #include "esphome/components/audio/audio.h" @@ -294,13 +298,21 @@ void I2SAudioSpeaker::speaker_task(void *params) { // Audio stream info changed, stop the speaker task so it will restart with the proper settings. break; } - +#ifdef USE_I2S_LEGACY i2s_event_t i2s_event; while (xQueueReceive(this_speaker->i2s_event_queue_, &i2s_event, 0)) { if (i2s_event.type == I2S_EVENT_TX_Q_OVF) { tx_dma_underflow = true; } } +#else + bool overflow; + while (xQueueReceive(this_speaker->i2s_event_queue_, &overflow, 0)) { + if (overflow) { + tx_dma_underflow = true; + } + } +#endif if (this_speaker->pause_state_) { // Pause state is accessed atomically, so thread safe @@ -319,6 +331,18 @@ void I2SAudioSpeaker::speaker_task(void *params) { bytes_read / sizeof(int16_t), this_speaker->q15_volume_factor_); } +#ifdef USE_ESP32_VARIANT_ESP32 + // For ESP32 8/16 bit mono mode samples need to be switched. + if (audio_stream_info.get_channels() == 1 && audio_stream_info.get_bits_per_sample() <= 16) { + size_t len = bytes_read / sizeof(int16_t); + int16_t *tmp_buf = (int16_t *) this_speaker->data_buffer_; + for (int i = 0; i < len; i += 2) { + int16_t tmp = tmp_buf[i]; + tmp_buf[i] = tmp_buf[i + 1]; + tmp_buf[i + 1] = tmp; + } + } +#endif // Write the audio data to a single DMA buffer at a time to reduce latency for the audio duration played // callback. const uint32_t batches = (bytes_read + single_dma_buffer_input_size - 1) / single_dma_buffer_input_size; @@ -327,6 +351,7 @@ void I2SAudioSpeaker::speaker_task(void *params) { size_t bytes_written = 0; size_t bytes_to_write = std::min(single_dma_buffer_input_size, bytes_read); +#ifdef USE_I2S_LEGACY if (audio_stream_info.get_bits_per_sample() == (uint8_t) this_speaker->bits_per_sample_) { i2s_write(this_speaker->parent_->get_port(), this_speaker->data_buffer_ + i * single_dma_buffer_input_size, bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5)); @@ -336,6 +361,10 @@ void I2SAudioSpeaker::speaker_task(void *params) { audio_stream_info.get_bits_per_sample(), this_speaker->bits_per_sample_, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5)); } +#else + i2s_channel_write(this_speaker->tx_handle_, this_speaker->data_buffer_ + i * single_dma_buffer_input_size, + bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5)); +#endif uint32_t write_timestamp = micros(); @@ -369,8 +398,12 @@ void I2SAudioSpeaker::speaker_task(void *params) { } xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STOPPING); - +#ifdef USE_I2S_LEGACY i2s_driver_uninstall(this_speaker->parent_->get_port()); +#else + i2s_channel_disable(this_speaker->tx_handle_); + i2s_del_channel(this_speaker->tx_handle_); +#endif this_speaker->parent_->unlock(); } @@ -462,12 +495,21 @@ esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t rin } esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) { +#ifdef USE_I2S_LEGACY if ((this->i2s_mode_ & I2S_MODE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT +#else + if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT +#endif // Can't reconfigure I2S bus, so the sample rate must match the configured value return ESP_ERR_NOT_SUPPORTED; } +#ifdef USE_I2S_LEGACY if ((i2s_bits_per_sample_t) audio_stream_info.get_bits_per_sample() > this->bits_per_sample_) { +#else + if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO && + (i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) { +#endif // Currently can't handle the case when the incoming audio has more bits per sample than the configured value return ESP_ERR_NOT_SUPPORTED; } @@ -476,6 +518,9 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea return ESP_ERR_INVALID_STATE; } + uint32_t dma_buffer_length = audio_stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS); + +#ifdef USE_I2S_LEGACY i2s_channel_fmt_t channel = this->channel_; if (audio_stream_info.get_channels() == 1) { @@ -488,8 +533,6 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea channel = I2S_CHANNEL_FMT_RIGHT_LEFT; } - int dma_buffer_length = audio_stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS); - i2s_driver_config_t config = { .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_TX), .sample_rate = audio_stream_info.get_sample_rate(), @@ -498,7 +541,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea .communication_format = this->i2s_comm_fmt_, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = DMA_BUFFERS_COUNT, - .dma_buf_len = dma_buffer_length, + .dma_buf_len = (int) dma_buffer_length, .use_apll = this->use_apll_, .tx_desc_auto_clear = true, .fixed_mclk = I2S_PIN_NO_CHANGE, @@ -545,6 +588,89 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea i2s_driver_uninstall(this->parent_->get_port()); this->parent_->unlock(); } +#else + i2s_chan_config_t chan_cfg = { + .id = this->parent_->get_port(), + .role = this->i2s_role_, + .dma_desc_num = DMA_BUFFERS_COUNT, + .dma_frame_num = dma_buffer_length, + .auto_clear = true, + }; + /* Allocate a new TX channel and get the handle of this channel */ + esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL); + if (err != ESP_OK) { + this->parent_->unlock(); + return err; + } + + i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT; +#ifdef I2S_CLK_SRC_APLL + if (this->use_apll_) { + clk_src = I2S_CLK_SRC_APLL; + } +#endif + i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config(); + + i2s_std_clk_config_t clk_cfg = { + .sample_rate_hz = audio_stream_info.get_sample_rate(), + .clk_src = clk_src, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + }; + + i2s_slot_mode_t slot_mode = this->slot_mode_; + i2s_std_slot_mask_t slot_mask = this->std_slot_mask_; + if (audio_stream_info.get_channels() == 1) { + slot_mode = I2S_SLOT_MODE_MONO; + } else if (audio_stream_info.get_channels() == 2) { + slot_mode = I2S_SLOT_MODE_STEREO; + slot_mask = I2S_STD_SLOT_BOTH; + } + + i2s_std_slot_config_t std_slot_cfg; + if (this->i2s_comm_fmt_ == "std") { + std_slot_cfg = + I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode); + } else if (this->i2s_comm_fmt_ == "pcm") { + std_slot_cfg = + I2S_STD_PCM_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode); + } else { + std_slot_cfg = + I2S_STD_MSB_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode); + } + std_slot_cfg.slot_bit_width = this->slot_bit_width_; + std_slot_cfg.slot_mask = slot_mask; + + pin_config.dout = this->dout_pin_; + + i2s_std_config_t std_cfg = { + .clk_cfg = clk_cfg, + .slot_cfg = std_slot_cfg, + .gpio_cfg = pin_config, + }; + /* Initialize the channel */ + err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg); + + if (err != ESP_OK) { + i2s_del_channel(this->tx_handle_); + this->parent_->unlock(); + return err; + } + if (this->i2s_event_queue_ == nullptr) { + this->i2s_event_queue_ = xQueueCreate(1, sizeof(bool)); + } + const i2s_event_callbacks_t callbacks = { + .on_send_q_ovf = i2s_overflow_cb, + }; + + i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this); + + /* Before reading data, start the TX channel first */ + i2s_channel_enable(this->tx_handle_); + if (err != ESP_OK) { + i2s_del_channel(this->tx_handle_); + this->parent_->unlock(); + } +#endif return err; } @@ -564,6 +690,15 @@ void I2SAudioSpeaker::delete_task_(size_t buffer_size) { vTaskDelete(nullptr); } +#ifndef USE_I2S_LEGACY +bool IRAM_ATTR I2SAudioSpeaker::i2s_overflow_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) { + I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) user_ctx; + bool overflow = true; + xQueueOverwrite(this_speaker->i2s_event_queue_, &overflow); + return false; +} +#endif + } // namespace i2s_audio } // namespace esphome diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index 7b14a57aac..b5e4b94bc4 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -4,8 +4,6 @@ #include "../i2s_audio.h" -#include - #include #include #include @@ -30,11 +28,16 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; } void set_timeout(uint32_t ms) { this->timeout_ = ms; } - void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; } +#ifdef USE_I2S_LEGACY #if SOC_I2S_SUPPORTS_DAC void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; } #endif + void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; } void set_i2s_comm_fmt(i2s_comm_format_t mode) { this->i2s_comm_fmt_ = mode; } +#else + void set_dout_pin(uint8_t pin) { this->dout_pin_ = (gpio_num_t) pin; } + void set_i2s_comm_fmt(std::string mode) { this->i2s_comm_fmt_ = std::move(mode); } +#endif void start() override; void stop() override; @@ -86,6 +89,10 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp /// @return True if an ERR_ESP bit is set and false if err == ESP_OK bool send_esp_err_to_event_group_(esp_err_t err); +#ifndef USE_I2S_LEGACY + static bool i2s_overflow_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx); +#endif + /// @brief Allocates the data buffer and ring buffer /// @param data_buffer_size Number of bytes to allocate for the data buffer. /// @param ring_buffer_size Number of bytes to allocate for the ring buffer. @@ -121,7 +128,6 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp uint32_t buffer_duration_ms_; optional timeout_; - uint8_t dout_pin_; bool task_created_{false}; bool pause_state_{false}; @@ -130,10 +136,17 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp size_t bytes_written_{0}; +#ifdef USE_I2S_LEGACY #if SOC_I2S_SUPPORTS_DAC i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE}; #endif + uint8_t dout_pin_; i2s_comm_format_t i2s_comm_fmt_; +#else + gpio_num_t dout_pin_; + std::string i2s_comm_fmt_; + i2s_chan_handle_t tx_handle_; +#endif uint32_t accumulated_frames_written_{0}; }; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index d6c2bf25e6..81ff6999ba 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -115,6 +115,7 @@ #ifdef USE_ARDUINO #define USE_PROMETHEUS #define USE_WIFI_WPA2_EAP +#define USE_I2S_LEGACY #endif // IDF-specific feature flags diff --git a/tests/components/micro_wake_word/common.yaml b/tests/components/micro_wake_word/common.yaml index 8bd7345307..c5422baa67 100644 --- a/tests/components/micro_wake_word/common.yaml +++ b/tests/components/micro_wake_word/common.yaml @@ -8,6 +8,7 @@ microphone: i2s_din_pin: GPIO17 adc_type: external pdm: true + bits_per_sample: 16bit micro_wake_word: on_wake_word_detected: diff --git a/tests/components/microphone/test.esp32-idf.yaml b/tests/components/microphone/test.esp32-idf.yaml index 392df582cc..fe9feb9888 100644 --- a/tests/components/microphone/test.esp32-idf.yaml +++ b/tests/components/microphone/test.esp32-idf.yaml @@ -4,9 +4,18 @@ substitutions: i2s_mclk_pin: GPIO17 i2s_din_pin: GPIO33 -<<: !include common.yaml +i2s_audio: + i2s_bclk_pin: ${i2s_bclk_pin} + i2s_lrclk_pin: ${i2s_lrclk_pin} + i2s_mclk_pin: ${i2s_mclk_pin} + use_legacy: true microphone: + - platform: i2s_audio + id: mic_id_external + i2s_din_pin: ${i2s_din_pin} + adc_type: external + pdm: false - platform: i2s_audio id: mic_id_adc adc_pin: 32