From ce3931e526bd29eb7c09d0d13b1a1abd6d3e66a5 Mon Sep 17 00:00:00 2001 From: Justin Bunton Date: Thu, 8 Oct 2026 13:31:06 -0400 Subject: [PATCH 1/3] Switch full-duplex i2s_audio to esphome/esphome#19959 Replace the vendored i2s_audio (2026.9.0 + #16882 with local fixes) with 2026.9.1's i2s_audio plus the full duplex changes from #19959, the implementation on track upstream. The bus now allocates the TX/RX pair once at boot and initializes RX first, so RX drives the shared clocks and MCLK for the PCM1808. TX starts and stops with each playback session from a fresh preload, which keeps speaker timestamps exact without the old realign workaround. Only the PR's own changes are applied; its dev base needs a newer esp-audio-libs and audio_dac than 2026.9.1. Core.yaml is unchanged and already meets the PR's validation rules. --- .../ESPHome/components/i2s_audio/README.md | 29 +- .../ESPHome/components/i2s_audio/__init__.py | 133 ++++--- .../components/i2s_audio/i2s_audio.cpp | 364 +++--------------- .../ESPHome/components/i2s_audio/i2s_audio.h | 121 +++--- .../i2s_audio/microphone/__init__.py | 3 - .../microphone/i2s_audio_microphone.cpp | 134 +++---- .../microphone/i2s_audio_microphone.h | 9 + .../components/i2s_audio/speaker/__init__.py | 7 +- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 96 +++-- .../i2s_audio/speaker/i2s_audio_speaker.h | 12 + .../speaker/i2s_audio_speaker_standard.cpp | 101 ++--- .../speaker/i2s_audio_speaker_standard.h | 14 + 12 files changed, 408 insertions(+), 615 deletions(-) diff --git a/Integrations/ESPHome/components/i2s_audio/README.md b/Integrations/ESPHome/components/i2s_audio/README.md index 0d3bb68..ef6d441 100644 --- a/Integrations/ESPHome/components/i2s_audio/README.md +++ b/Integrations/ESPHome/components/i2s_audio/README.md @@ -1,19 +1,14 @@ # i2s_audio (full duplex) -ESPHome 2026.9.0's `i2s_audio` with full duplex support, so the PCM5122 DAC and -PCM1808 ADC can share one I2S port (common WS/BCLK/MCLK, separate DOUT/DIN). - -Based on esphome/esphome#16882 (head `3db4594`), with these changes: - -- The speaker joins a TX channel the microphone already started without - skewing its playback timestamps (Sendspin sync), and realigns its write - position each session since TX is never reset. -- The DMA ring is fixed at the speaker's 5 x 10 ms, whichever side allocates - first, so stall tolerance doesn't depend on start order. -- TX always auto-clears, so the DAC plays silence rather than looping the last - buffer when the microphone allocated the channels first. -- Both channels get both data pins, whichever side initializes first. -- Mismatched speaker/microphone formats are rejected at config time and at - runtime, instead of playing at the wrong speed. - -Drop this override once full duplex lands upstream. +ESPHome 2026.9.1's `i2s_audio` with full duplex support from esphome/esphome#19959 +(head `04d4759`), so the PCM5122 DAC and PCM1808 ADC can share one I2S port +(common WS/BCLK/MCLK, separate DOUT/DIN). + +Only the PR's own changes are applied, on top of 2026.9.1. The PR is built on +ESPHome dev, and dev's other `i2s_audio` changes need a newer esp-audio-libs and +`audio_dac` than 2026.9.1 has. One PR hunk is left out: a guard in the speaker's +`setup()` that skips parking the data pin on a full duplex bus. 2026.9.1 doesn't +park the pin there, so there's nothing to skip. + +Delete this folder and the `external_components` block in Core.yaml once #19959 +ships in an ESPHome release. diff --git a/Integrations/ESPHome/components/i2s_audio/__init__.py b/Integrations/ESPHome/components/i2s_audio/__init__.py index 2bea84d..2361eb2 100644 --- a/Integrations/ESPHome/components/i2s_audio/__init__.py +++ b/Integrations/ESPHome/components/i2s_audio/__init__.py @@ -18,8 +18,17 @@ VARIANT_ESP32S2, VARIANT_ESP32S3, ) +from esphome.config_helpers import filter_source_files_from_defines 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_MICROPHONE, + CONF_PLATFORM, + CONF_SAMPLE_RATE, + CONF_SPEAKER, +) from esphome.core import CORE from esphome.cpp_generator import MockObj, MockObjClass import esphome.final_validate as fv @@ -41,9 +50,11 @@ CONF_I2S_AUDIO = "i2s_audio" CONF_I2S_AUDIO_ID = "i2s_audio_id" +CONF_FULL_DUPLEX = "full_duplex" +CONF_I2S_COMM_FMT = "i2s_comm_fmt" +CONF_SPDIF_MODE = "spdif_mode" CONF_I2S_MODE = "i2s_mode" -CONF_FULL_DUPLEX = "full_duplex" CONF_PRIMARY = "primary" CONF_SECONDARY = "secondary" @@ -200,6 +211,13 @@ async def register_i2s_audio_component(var: MockObj, config: ConfigType) -> None cg.add(var.set_use_apll(config[CONF_USE_APLL])) cg.add(var.set_mclk_multiple(I2S_MCLK_MULTIPLE[config[CONF_MCLK_MULTIPLE]])) + if str(config[CONF_I2S_AUDIO_ID]) in _get_data().full_duplex_buses: + parent = await cg.get_variable(config[CONF_I2S_AUDIO_ID]) + if config[CONF_ID].type.inherits_from(I2SAudioIn): + cg.add(parent.set_audio_in(var)) + else: + cg.add(parent.set_audio_out(var)) + CONFIG_SCHEMA = cv.Schema( { @@ -217,6 +235,7 @@ class I2SAudioData: """I2S audio component state stored in CORE.data.""" port_map: dict[str, int] = field(default_factory=dict) + full_duplex_buses: set[str] = field(default_factory=set) def _get_data() -> I2SAudioData: @@ -263,56 +282,56 @@ def _assign_ports() -> None: next_port += 1 -def _validate_full_duplex(i2s_audio_configs: list[ConfigType]) -> None: - """Full duplex runs TX and RX from one clock and slot layout, so every user must agree on the format.""" - full_config = fv.full_config.get() - for bus in i2s_audio_configs: - if not bus[CONF_FULL_DUPLEX]: - continue - bus_id = str(bus[CONF_ID]) - users = [ - (domain, conf) - for domain in ("speaker", "microphone") - for conf in full_config.get(domain, []) - if conf.get("platform") == CONF_I2S_AUDIO - and str(conf.get(CONF_I2S_AUDIO_ID)) == bus_id - ] - speakers = [conf for domain, conf in users if domain == "speaker"] - microphones = [conf for domain, conf in users if domain == "microphone"] - if len(speakers) > 1 or len(microphones) > 1: - raise cv.Invalid( - f"Full duplex bus '{bus_id}' supports one speaker and one microphone" - ) - if any(conf.get("spdif_mode") for conf in speakers): - raise cv.Invalid(f"Full duplex bus '{bus_id}' does not support SPDIF mode") - if any(conf.get(CONF_PDM) for conf in microphones): - raise cv.Invalid(f"Full duplex bus '{bus_id}' does not support PDM") - # The microphone is always Philips I2S, and both channels share one slot config - if any(conf.get("i2s_comm_fmt", "stand_i2s") != "stand_i2s" for conf in speakers): - raise cv.Invalid( - f"Full duplex bus '{bus_id}' requires 'i2s_comm_fmt: stand_i2s'" - ) - - def _format(conf: ConfigType) -> tuple: - slot_mode = CONF_STEREO if conf[CONF_CHANNEL] == CONF_STEREO else CONF_MONO - return ( - conf[CONF_SAMPLE_RATE], - conf[CONF_BITS_PER_SAMPLE], - slot_mode, - conf[CONF_MCLK_MULTIPLE], - conf[CONF_I2S_MODE], - ) - - formats = {_format(conf) for _, conf in users} - if len(formats) > 1: - raise cv.Invalid( - f"Full duplex bus '{bus_id}' needs the speaker and microphone to share sample_rate, " - f"bits_per_sample, stereo/mono channel layout, mclk_multiple and i2s_mode" - ) +def _bus_devices(full_config: ConfigType, domain: str, bus_id: str) -> list[ConfigType]: + return [ + device + for device in full_config.get(domain, []) + if device.get(CONF_PLATFORM) == CONF_I2S_AUDIO + and str(device[CONF_I2S_AUDIO_ID]) == bus_id + ] + + +def _validate_full_duplex(full_config: ConfigType, bus_id: str) -> None: + """Check that a full duplex bus has one microphone and speakers that can share its clocks and TX channel.""" + microphones = _bus_devices(full_config, CONF_MICROPHONE, bus_id) + speakers = _bus_devices(full_config, CONF_SPEAKER, bus_id) + if len(microphones) != 1 or not speakers: + raise cv.Invalid( + f"'{CONF_FULL_DUPLEX}' requires exactly one i2s_audio microphone and at least one " + f"i2s_audio speaker on bus '{bus_id}'" + ) + microphone = microphones[0] + if microphone.get(CONF_PDM): + raise cv.Invalid(f"A PDM microphone cannot use a '{CONF_FULL_DUPLEX}' bus") + for speaker in speakers: + if speaker.get(CONF_SPDIF_MODE): + raise cv.Invalid(f"An SPDIF speaker cannot use a '{CONF_FULL_DUPLEX}' bus") + # Both directions run from the same bit and word clocks + for key in ( + CONF_SAMPLE_RATE, + CONF_BITS_PER_SAMPLE, + CONF_I2S_MODE, + CONF_USE_APLL, + CONF_MCLK_MULTIPLE, + ): + if microphone[key] != speaker[key]: + raise cv.Invalid( + f"The microphone and speaker '{speaker[CONF_ID]}' on '{CONF_FULL_DUPLEX}' bus " + f"'{bus_id}' must use the same '{key}'" + ) + # The TX channel is set up once from one speaker's configuration, and the speakers take turns using it + first = speakers[0] + for speaker in speakers[1:]: + for key in (CONF_I2S_DOUT_PIN, CONF_CHANNEL, CONF_I2S_COMM_FMT): + if speaker.get(key) != first.get(key): + raise cv.Invalid( + f"The speakers on '{CONF_FULL_DUPLEX}' bus '{bus_id}' must use the same '{key}'" + ) def _final_validate(_: ConfigType) -> None: - i2s_audio_configs = fv.full_config.get()[CONF_I2S_AUDIO] + full_config = fv.full_config.get() + i2s_audio_configs = full_config[CONF_I2S_AUDIO] variant = get_esp32_variant() if variant not in I2S_PORTS: raise cv.Invalid(f"Unsupported variant {variant}") @@ -320,9 +339,15 @@ def _final_validate(_: ConfigType) -> None: raise cv.Invalid( f"Only {I2S_PORTS[variant]} I2S audio ports are supported on {variant}" ) - _validate_full_duplex(i2s_audio_configs) _assign_ports() + data = _get_data() + for config in i2s_audio_configs: + if config[CONF_FULL_DUPLEX]: + bus_id = str(config[CONF_ID]) + _validate_full_duplex(full_config, bus_id) + data.full_duplex_buses.add(bus_id) + FINAL_VALIDATE_SCHEMA = _final_validate @@ -349,4 +374,10 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) if CONF_I2S_MCLK_PIN in config: cg.add(var.set_mclk_pin(config[CONF_I2S_MCLK_PIN])) - cg.add(var.set_full_duplex(config[CONF_FULL_DUPLEX])) + if config[CONF_FULL_DUPLEX]: + cg.add_define("USE_I2S_AUDIO_FULL_DUPLEX") + + +FILTER_SOURCE_FILES = filter_source_files_from_defines( + {"i2s_audio.cpp": "USE_I2S_AUDIO_FULL_DUPLEX"} +) diff --git a/Integrations/ESPHome/components/i2s_audio/i2s_audio.cpp b/Integrations/ESPHome/components/i2s_audio/i2s_audio.cpp index 2303bff..e8b4edb 100644 --- a/Integrations/ESPHome/components/i2s_audio/i2s_audio.cpp +++ b/Integrations/ESPHome/components/i2s_audio/i2s_audio.cpp @@ -1,339 +1,101 @@ #include "i2s_audio.h" -#ifdef USE_ESP32 +#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_FULL_DUPLEX) #include "esphome/core/log.h" -#include -#include -#include - -#include -#include "esp_rom_sys.h" -#include "esp_timer.h" - namespace esphome::i2s_audio { static const char *const TAG = "i2s_audio"; -// The shared DMA ring is fixed rather than taken from whichever side allocates first, so the speaker keeps its -// usual stall tolerance even when the microphone starts first. Matches the standard speaker's 5 x 10 ms ring. -static constexpr uint32_t FULL_DUPLEX_DMA_BUFFER_DURATION_MS = 10; -static constexpr uint32_t FULL_DUPLEX_DMA_BUFFERS_COUNT = 5; -#if SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE -static constexpr uint32_t FULL_DUPLEX_DMA_BUFFER_MAX_SIZE = DMA_DESCRIPTOR_BUFFER_MAX_SIZE_64B_ALIGNED; -#else -static constexpr uint32_t FULL_DUPLEX_DMA_BUFFER_MAX_SIZE = DMA_DESCRIPTOR_BUFFER_MAX_SIZE_4B_ALIGNED; -#endif -// Long enough for an ISR that fired on_sent before a queue reset to also release its buffer -static constexpr uint32_t FULL_DUPLEX_ISR_SETTLE_US = 50; +void I2SAudioComponent::setup() { + if (!this->is_full_duplex()) + return; -static uint32_t effective_slot_bit_width(const i2s_std_slot_config_t &slot_cfg) { - if (slot_cfg.slot_bit_width == I2S_SLOT_BIT_WIDTH_AUTO) { - return static_cast(slot_cfg.data_bit_width); + // ESP-IDF only shares the bit and word clocks between channels allocated in the same call, and deleting either + // channel ends the pairing. Allocate both once here and keep them for the lifetime of the device. + i2s_chan_config_t chan_cfg{}; + i2s_std_config_t tx_cfg{}; + i2s_std_config_t rx_cfg{}; + if (!this->audio_out_->build_full_duplex_config(chan_cfg, tx_cfg) || + !this->audio_in_->build_full_duplex_config(rx_cfg)) { + ESP_LOGE(TAG, "Microphone or speaker does not support full duplex"); + this->mark_failed(); + return; } - return static_cast(slot_cfg.slot_bit_width); -} -esp_err_t I2SAudioComponent::allocate_full_duplex_channels_(const i2s_chan_config_t &chan_cfg, - const i2s_std_config_t &std_cfg) { - if (this->rx_handle_ != nullptr && this->tx_handle_ != nullptr) { - return ESP_OK; - } - if (!this->full_duplex_) { - return ESP_ERR_INVALID_STATE; + esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, &this->rx_handle_); + if (err == ESP_OK) { + // The first initialized channel drives the shared clocks. Use RX so the microphone runs on its own. + err = i2s_channel_init_std_mode(this->rx_handle_, &rx_cfg); } - if (this->din_pin_ == I2S_GPIO_UNUSED || this->dout_pin_ == I2S_GPIO_UNUSED) { - ESP_LOGE(TAG, "Full duplex requires both DIN and DOUT pins on the same I2S bus"); - return ESP_ERR_INVALID_ARG; + if (err == ESP_OK) { + err = i2s_channel_init_std_mode(this->tx_handle_, &tx_cfg); } - - // Both channels share this config. If the microphone allocates first, its config has auto clear off, and - // the DAC would then loop the last buffer whenever the speaker stops writing - i2s_chan_config_t duplex_cfg = chan_cfg; - duplex_cfg.auto_clear = true; - const uint32_t frame_bytes = (static_cast(std_cfg.slot_cfg.data_bit_width) / 8) * - (std_cfg.slot_cfg.slot_mode == I2S_SLOT_MODE_STEREO ? 2 : 1); - duplex_cfg.dma_desc_num = FULL_DUPLEX_DMA_BUFFERS_COUNT; - duplex_cfg.dma_frame_num = std::min(std_cfg.clk_cfg.sample_rate_hz * FULL_DUPLEX_DMA_BUFFER_DURATION_MS / 1000, - FULL_DUPLEX_DMA_BUFFER_MAX_SIZE / frame_bytes); - - esp_err_t err = i2s_new_channel(&duplex_cfg, &this->tx_handle_, &this->rx_handle_); if (err != ESP_OK) { - ESP_LOGE(TAG, "Full duplex channel allocation failed: %s", esp_err_to_name(err)); - this->tx_handle_ = nullptr; - this->rx_handle_ = nullptr; - this->full_duplex_dma_desc_num_ = 0; - } else { - this->full_duplex_dma_desc_num_ = duplex_cfg.dma_desc_num; - } - return err; -} - -esp_err_t I2SAudioComponent::initialize_full_duplex_channels_(const i2s_std_config_t &std_cfg) { - if (this->tx_handle_ == nullptr || this->rx_handle_ == nullptr) { - return ESP_ERR_INVALID_STATE; - } - - if (this->tx_channel_initialized_) { - // Channels keep the first user's clock and slot layout; a different format would play at the wrong speed - if (std_cfg.clk_cfg.sample_rate_hz != this->full_duplex_sample_rate_ || - static_cast(std_cfg.slot_cfg.data_bit_width) != this->full_duplex_data_bit_width_ || - effective_slot_bit_width(std_cfg.slot_cfg) != this->full_duplex_slot_bit_width_ || - std_cfg.slot_cfg.slot_mode != this->full_duplex_slot_mode_) { - ESP_LOGE(TAG, - "Full duplex format mismatch: bus runs %" PRIu32 " Hz, %" PRIu32 "-bit data in %" PRIu32 - "-bit %s slots, requested %" PRIu32 " Hz, %" PRIu32 "-bit data in %" PRIu32 "-bit %s slots", - this->full_duplex_sample_rate_, this->full_duplex_data_bit_width_, this->full_duplex_slot_bit_width_, - this->full_duplex_slot_mode_ == I2S_SLOT_MODE_STEREO ? "stereo" : "mono", - std_cfg.clk_cfg.sample_rate_hz, static_cast(std_cfg.slot_cfg.data_bit_width), - effective_slot_bit_width(std_cfg.slot_cfg), - std_cfg.slot_cfg.slot_mode == I2S_SLOT_MODE_STEREO ? "stereo" : "mono"); - return ESP_ERR_INVALID_ARG; + ESP_LOGE(TAG, "Full duplex channel setup failed: %s", esp_err_to_name(err)); + if (this->tx_handle_ != nullptr) { + i2s_del_channel(this->tx_handle_); + this->tx_handle_ = nullptr; } - } - - // Each user's pin config only names its own data pin, but both channels are initialized here - i2s_std_config_t duplex_cfg = std_cfg; - duplex_cfg.gpio_cfg = this->get_full_duplex_pin_config(); - - if (!this->tx_channel_initialized_) { - esp_err_t err = i2s_channel_init_std_mode(this->tx_handle_, &duplex_cfg); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to initialize full duplex TX channel: %s", esp_err_to_name(err)); - return err; + if (this->rx_handle_ != nullptr) { + i2s_del_channel(this->rx_handle_); + this->rx_handle_ = nullptr; } - this->tx_channel_initialized_ = true; - this->full_duplex_sample_rate_ = std_cfg.clk_cfg.sample_rate_hz; - this->full_duplex_data_bit_width_ = static_cast(std_cfg.slot_cfg.data_bit_width); - this->full_duplex_slot_bit_width_ = effective_slot_bit_width(std_cfg.slot_cfg); - this->full_duplex_slot_mode_ = std_cfg.slot_cfg.slot_mode; + this->mark_failed(); } - - if (!this->rx_channel_initialized_) { - esp_err_t err = i2s_channel_init_std_mode(this->rx_handle_, &duplex_cfg); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to initialize full duplex RX channel: %s", esp_err_to_name(err)); - return err; - } - this->rx_channel_initialized_ = true; - } - - return ESP_OK; } -esp_err_t I2SAudioComponent::setup_full_duplex_rx_channel(const i2s_chan_config_t &chan_cfg, - const i2s_std_config_t &std_cfg, - i2s_chan_handle_t *rx_handle) { - LockGuard guard(this->full_duplex_lock_); - esp_err_t err = this->allocate_full_duplex_channels_(chan_cfg, std_cfg); - if (err != ESP_OK) { - return err; - } - err = this->initialize_full_duplex_channels_(std_cfg); - if (err == ESP_OK) { - *rx_handle = this->rx_handle_; +i2s_chan_handle_t I2SAudioComponent::acquire_rx_channel() { + if (this->rx_handle_ == nullptr) + return nullptr; + this->rx_in_use_ = true; + if (!this->update_rx_channel_()) { + this->rx_in_use_ = false; + return nullptr; } - return err; + return this->rx_handle_; } -esp_err_t I2SAudioComponent::setup_full_duplex_tx_channel(const i2s_chan_config_t &chan_cfg, - const i2s_std_config_t &std_cfg, - i2s_chan_handle_t *tx_handle) { - LockGuard guard(this->full_duplex_lock_); - esp_err_t err = this->allocate_full_duplex_channels_(chan_cfg, std_cfg); - if (err != ESP_OK) { - return err; - } - err = this->initialize_full_duplex_channels_(std_cfg); - if (err == ESP_OK) { - *tx_handle = this->tx_handle_; - } - return err; +void I2SAudioComponent::release_rx_channel() { + this->rx_in_use_ = false; + this->update_rx_channel_(); } -size_t I2SAudioComponent::get_full_duplex_dma_buffer_bytes_() { - i2s_chan_info_t chan_info; - if (this->full_duplex_dma_desc_num_ == 0 || i2s_channel_get_info(this->tx_handle_, &chan_info) != ESP_OK) { - return 0; +i2s_chan_handle_t I2SAudioComponent::acquire_tx_channel() { + // Speakers sharing the bus take turns; a second one must not get the channel while the first is playing + if (this->tx_handle_ == nullptr || this->tx_in_use_) + return nullptr; + this->tx_in_use_ = true; + if (!this->update_rx_channel_()) { + this->tx_in_use_ = false; + return nullptr; } - return chan_info.total_dma_buf_size / this->full_duplex_dma_desc_num_; + return this->tx_handle_; } -// Caller holds full_duplex_lock_ -esp_err_t I2SAudioComponent::start_full_duplex_tx_(const uint8_t *silence, size_t buffer_bytes) { - esp_err_t err; - if (!this->full_duplex_tx_callback_registered_) { - // Must be registered before the channel is enabled - const i2s_event_callbacks_t callbacks = {.on_sent = I2SAudioComponent::full_duplex_on_sent_cb}; - err = i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Full duplex TX callback registration failed: %s", esp_err_to_name(err)); - return err; - } - this->full_duplex_tx_callback_registered_ = true; - } - - // Preload every descriptor so the first on_sent events pair with known silence - for (size_t i = 0; i < this->full_duplex_dma_desc_num_; i++) { - size_t bytes_loaded = 0; - err = i2s_channel_preload_data(this->tx_handle_, silence, buffer_bytes, &bytes_loaded); - if (err != ESP_OK || bytes_loaded != buffer_bytes) { - ESP_LOGE(TAG, "Full duplex TX silence preload failed: %s (%u/%u bytes)", esp_err_to_name(err), - (unsigned) bytes_loaded, (unsigned) buffer_bytes); - return err != ESP_OK ? err : ESP_ERR_INVALID_SIZE; - } - } - - err = i2s_channel_enable(this->tx_handle_); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Full duplex TX clock enable failed: %s", esp_err_to_name(err)); - return err; - } - this->full_duplex_tx_enabled_ = true; - ESP_LOGD(TAG, "Full duplex TX clock is running"); - return ESP_OK; +void I2SAudioComponent::release_tx_channel() { + // The speaker task may have left the channel disabled already, so an invalid state error is expected here + i2s_channel_disable(this->tx_handle_); + this->tx_in_use_ = false; + this->update_rx_channel_(); } -esp_err_t I2SAudioComponent::ensure_full_duplex_tx_running() { - LockGuard guard(this->full_duplex_lock_); - if (!this->full_duplex_ || this->tx_handle_ == nullptr || !this->tx_channel_initialized_) { - return ESP_ERR_INVALID_STATE; - } - if (this->full_duplex_tx_enabled_) { - return ESP_OK; - } - - const size_t buffer_bytes = this->get_full_duplex_dma_buffer_bytes_(); - if (buffer_bytes == 0) { - ESP_LOGE(TAG, "Full duplex TX clock start failed: no DMA buffer size available"); - return ESP_ERR_INVALID_SIZE; - } - - RAMAllocator allocator; - uint8_t *silence = allocator.allocate(buffer_bytes); - if (silence == nullptr) { - return ESP_ERR_NO_MEM; - } - memset(silence, 0, buffer_bytes); - esp_err_t err = this->start_full_duplex_tx_(silence, buffer_bytes); - allocator.deallocate(silence, buffer_bytes); - return err; -} - -esp_err_t I2SAudioComponent::attach_full_duplex_speaker(QueueHandle_t event_queue, QueueHandle_t records_queue, - EventGroupHandle_t event_group, EventBits_t overflow_bits, - const uint8_t *silence, size_t buffer_bytes) { - // Every descriptor finishes once before the speaker's first write reaches the wire, in both paths below - auto seed_records = [&]() -> bool { - const uint32_t zero_real_frames = 0; - for (size_t i = 0; i < this->full_duplex_dma_desc_num_; i++) { - if (xQueueSend(records_queue, &zero_real_frames, 0) != pdTRUE) { - return false; - } - } +bool I2SAudioComponent::update_rx_channel_() { + const bool needed = this->rx_in_use_ || this->tx_in_use_; + if (needed == this->rx_enabled_) return true; - }; - - { - LockGuard guard(this->full_duplex_lock_); - if (!this->full_duplex_ || this->tx_handle_ == nullptr || !this->tx_channel_initialized_) { - return ESP_ERR_INVALID_STATE; - } - - this->full_duplex_tx_event_group_ = event_group; - this->full_duplex_tx_overflow_bits_ = overflow_bits; - - if (!this->full_duplex_tx_enabled_) { - // Fresh start, identical to a simplex speaker: preload silence, then enable - if (!seed_records()) { - return ESP_ERR_NO_MEM; - } - this->full_duplex_tx_event_queue_.store(event_queue, std::memory_order_release); - esp_err_t err = this->start_full_duplex_tx_(silence, buffer_bytes); - if (err != ESP_OK) { - this->full_duplex_tx_event_queue_.store(nullptr, std::memory_order_release); - } - return err; - } - } - - // TX is already clocking the microphone, so its DMA ring holds buffers it has released but nobody refilled. - // A write would land in one of those and play early, skewing timestamps by up to a whole ring. Claim them all - // with silence so the next write waits for the buffer on the wire, the same position as a fresh preload. - // - // First let one full ring play out untouched. That fills the driver's release queue, and a full queue makes - // its next write start a fresh buffer, discarding any partial write position a previous session left behind. - // The lock is released meanwhile so microphone start/stop on the main loop isn't stalled; that's safe because - // TX is never disabled once enabled and only this speaker task writes to it. - vTaskDelay(pdMS_TO_TICKS(this->full_duplex_dma_desc_num_ * FULL_DUPLEX_DMA_BUFFER_DURATION_MS + 1)); - LockGuard guard(this->full_duplex_lock_); - - // Events that arrive meanwhile are discarded; the loop exits once nothing was released since the last reset - this->full_duplex_tx_event_queue_.store(event_queue, std::memory_order_release); - size_t bytes_written = 0; - do { - while (i2s_channel_write(this->tx_handle_, silence, buffer_bytes, &bytes_written, 0) == ESP_OK) { - } - xQueueReset(event_queue); - esp_rom_delay_us(FULL_DUPLEX_ISR_SETTLE_US); - } while (i2s_channel_write(this->tx_handle_, silence, buffer_bytes, &bytes_written, 0) == ESP_OK); - if (!seed_records()) { - this->full_duplex_tx_event_queue_.store(nullptr, std::memory_order_release); - return ESP_ERR_NO_MEM; - } - return ESP_OK; -} - -void I2SAudioComponent::detach_full_duplex_tx_event_queue(QueueHandle_t queue) { - LockGuard guard(this->full_duplex_lock_); - QueueHandle_t expected = queue; - this->full_duplex_tx_event_queue_.compare_exchange_strong(expected, nullptr, std::memory_order_acq_rel); -} - -bool IRAM_ATTR I2SAudioComponent::full_duplex_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, - void *user_ctx) { - (void) handle; - (void) event; - auto *component = static_cast(user_ctx); - QueueHandle_t queue = component->full_duplex_tx_event_queue_.load(std::memory_order_acquire); - if (queue == nullptr) { + esp_err_t err = needed ? i2s_channel_enable(this->rx_handle_) : i2s_channel_disable(this->rx_handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to %s RX channel: %s", needed ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable"), + esp_err_to_name(err)); return false; } - - int64_t now = esp_timer_get_time(); - BaseType_t need_yield1 = pdFALSE; - BaseType_t need_yield2 = pdFALSE; - BaseType_t need_yield3 = pdFALSE; - - // Mirrors I2SAudioSpeakerBase::i2s_on_sent_cb: a dropped event desyncs the speaker's records, so it restarts - if (xQueueIsQueueFullFromISR(queue)) { - int64_t dummy; - xQueueReceiveFromISR(queue, &dummy, &need_yield1); - if (component->full_duplex_tx_event_group_ != nullptr && component->full_duplex_tx_overflow_bits_ != 0) { - xEventGroupSetBitsFromISR(component->full_duplex_tx_event_group_, component->full_duplex_tx_overflow_bits_, - &need_yield2); - } - } - - xQueueSendToBackFromISR(queue, &now, &need_yield3); - return need_yield1 | need_yield2 | need_yield3; -} - -void I2SAudioComponent::release_full_duplex_rx_channel(i2s_chan_handle_t rx_handle) { - LockGuard guard(this->full_duplex_lock_); - if (rx_handle == this->rx_handle_ && this->full_duplex_rx_enabled_) { - i2s_channel_disable(this->rx_handle_); - this->full_duplex_rx_enabled_ = false; - } -} - -void I2SAudioComponent::release_full_duplex_tx_channel(i2s_chan_handle_t tx_handle) { - (void) tx_handle; - // TX stays enabled: it clocks the external ADC, and stopping it would stall RX until playback resumes + this->rx_enabled_ = needed; + return true; } } // namespace esphome::i2s_audio -#endif // USE_ESP32 +#endif // USE_ESP32 && USE_I2S_AUDIO_FULL_DUPLEX diff --git a/Integrations/ESPHome/components/i2s_audio/i2s_audio.h b/Integrations/ESPHome/components/i2s_audio/i2s_audio.h index 97f6147..ffbebc5 100644 --- a/Integrations/ESPHome/components/i2s_audio/i2s_audio.h +++ b/Integrations/ESPHome/components/i2s_audio/i2s_audio.h @@ -7,9 +7,6 @@ #include "esphome/core/helpers.h" #include #include -#include -#include -#include namespace esphome::i2s_audio { @@ -35,12 +32,52 @@ class I2SAudioBase : public Parented { i2s_mclk_multiple_t mclk_multiple_; }; -class I2SAudioIn : public I2SAudioBase {}; +class I2SAudioIn : public I2SAudioBase { +#ifdef USE_I2S_AUDIO_FULL_DUPLEX + public: + /// @brief Builds the RX configuration the parent uses to set up a full duplex channel pair. + /// @return false if this input cannot share a full duplex bus + virtual bool build_full_duplex_config(i2s_std_config_t &std_cfg) = 0; +#endif +}; -class I2SAudioOut : public I2SAudioBase {}; +class I2SAudioOut : public I2SAudioBase { +#ifdef USE_I2S_AUDIO_FULL_DUPLEX + public: + /// @brief Builds the TX configuration the parent uses to set up a full duplex channel pair. The channel + /// configuration (DMA layout, role, interrupt priority) is shared by both channels. + /// @return false if this output cannot share a full duplex bus + virtual bool build_full_duplex_config(i2s_chan_config_t &chan_cfg, i2s_std_config_t &std_cfg) { return false; } +#endif +}; class I2SAudioComponent final : public Component { public: +#ifdef USE_I2S_AUDIO_FULL_DUPLEX + void setup() override; + + void set_audio_in(I2SAudioIn *audio_in) { this->audio_in_ = audio_in; } + void set_audio_out(I2SAudioOut *audio_out) { this->audio_out_ = audio_out; } + + /// @brief True when a microphone and a speaker share this bus at the same time. + bool is_full_duplex() const { return this->audio_in_ != nullptr && this->audio_out_ != nullptr; } + + /// @brief Enables the full duplex RX channel. Main loop only. + /// @return The enabled RX handle, or nullptr if the channel pair is unavailable + i2s_chan_handle_t acquire_rx_channel(); + /// @brief Releases the RX channel; it keeps running while the TX side needs its clocks. Main loop only. + void release_rx_channel(); + + /// @brief Starts the shared clocks and hands over the full duplex TX channel, still disabled, so the caller + /// can register callbacks and preload data before enabling it. Main loop only. + /// @return The TX handle, or nullptr if the channel pair is unavailable or another speaker holds it + i2s_chan_handle_t acquire_tx_channel(); + /// @brief Disables the TX channel and stops the shared clocks if the RX side is idle. Only the speaker that + /// acquired the channel may call this. Main loop only. + void release_tx_channel(); + /// @brief True while another speaker holds the full duplex TX channel + bool is_tx_in_use() const { return this->tx_in_use_; } +#endif i2s_std_gpio_config_t get_pin_config() const { return {.mclk = (gpio_num_t) this->mclk_pin_, .bclk = (gpio_num_t) this->bclk_pin_, @@ -53,27 +90,11 @@ class I2SAudioComponent final : public Component { .ws_inv = false, }}; } - i2s_std_gpio_config_t get_full_duplex_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 = (gpio_num_t) this->dout_pin_, - .din = (gpio_num_t) this->din_pin_, - .invert_flags = { - .mclk_inv = false, - .bclk_inv = false, - .ws_inv = false, - }}; - } void set_mclk_pin(int pin) { this->mclk_pin_ = pin; } void set_bclk_pin(int pin) { this->bclk_pin_ = pin; } void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; } - void set_din_pin(int pin) { this->din_pin_ = pin; } - void set_dout_pin(int pin) { this->dout_pin_ = pin; } void set_port(int port) { this->port_ = port; } - void set_full_duplex(bool full_duplex) { this->full_duplex_ = full_duplex; } - bool is_full_duplex() const { return this->full_duplex_; } #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) int get_port() const { return this->port_; } #else @@ -84,63 +105,25 @@ class I2SAudioComponent final : public Component { bool try_lock() { return this->lock_.try_lock(); } void unlock() { this->lock_.unlock(); } - // Full duplex: TX and RX are allocated together and share one clock/slot config, so the first user's config - // wins and a mismatched second user is rejected. TX stays running once started, since it clocks the ADC. - esp_err_t setup_full_duplex_rx_channel(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg, - i2s_chan_handle_t *rx_handle); - esp_err_t setup_full_duplex_tx_channel(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg, - i2s_chan_handle_t *tx_handle); - /// Starts the TX clock on silence if it isn't running yet (used by the microphone). - esp_err_t ensure_full_duplex_tx_running(); - /// Joins the TX stream for a speaker task. Leaves `records_queue` holding one zero-frame record per DMA buffer - /// that completes before the speaker's first write can play, then routes on_sent events into `event_queue`. - esp_err_t attach_full_duplex_speaker(QueueHandle_t event_queue, QueueHandle_t records_queue, - EventGroupHandle_t event_group, EventBits_t overflow_bits, - const uint8_t *silence, size_t buffer_bytes); - void detach_full_duplex_tx_event_queue(QueueHandle_t queue); - size_t get_full_duplex_dma_desc_num() const { return this->full_duplex_dma_desc_num_; } - void mark_full_duplex_rx_running() { this->full_duplex_rx_enabled_ = true; } - void release_full_duplex_rx_channel(i2s_chan_handle_t rx_handle); - void release_full_duplex_tx_channel(i2s_chan_handle_t tx_handle); - protected: - esp_err_t allocate_full_duplex_channels_(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg); - esp_err_t initialize_full_duplex_channels_(const i2s_std_config_t &std_cfg); - esp_err_t start_full_duplex_tx_(const uint8_t *silence, size_t buffer_bytes); - size_t get_full_duplex_dma_buffer_bytes_(); - static bool full_duplex_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx); - Mutex lock_; - // Serializes full duplex setup between the speaker and microphone tasks; lock_ is unused in full duplex mode - Mutex full_duplex_lock_; + +#ifdef USE_I2S_AUDIO_FULL_DUPLEX + /// @brief Enables the RX channel while either side is active, since it drives the shared clocks. + bool update_rx_channel_(); I2SAudioIn *audio_in_{nullptr}; I2SAudioOut *audio_out_{nullptr}; + i2s_chan_handle_t rx_handle_{nullptr}; + i2s_chan_handle_t tx_handle_{nullptr}; + bool rx_in_use_{false}; + bool tx_in_use_{false}; + bool rx_enabled_{false}; +#endif int mclk_pin_{I2S_GPIO_UNUSED}; int bclk_pin_{I2S_GPIO_UNUSED}; - int din_pin_{I2S_GPIO_UNUSED}; - int dout_pin_{I2S_GPIO_UNUSED}; int lrclk_pin_; int port_{}; - bool full_duplex_{false}; - - i2s_chan_handle_t rx_handle_{nullptr}; - i2s_chan_handle_t tx_handle_{nullptr}; - bool rx_channel_initialized_{false}; - bool tx_channel_initialized_{false}; - bool full_duplex_tx_enabled_{false}; - bool full_duplex_rx_enabled_{false}; - bool full_duplex_tx_callback_registered_{false}; - size_t full_duplex_dma_desc_num_{0}; - // Format of the first user, which both channels were initialized with - uint32_t full_duplex_sample_rate_{0}; - uint32_t full_duplex_data_bit_width_{0}; - uint32_t full_duplex_slot_bit_width_{0}; - i2s_slot_mode_t full_duplex_slot_mode_{}; - // Read by the TX ISR; event group and bits are published before the queue - std::atomic full_duplex_tx_event_queue_{nullptr}; - EventGroupHandle_t full_duplex_tx_event_group_{nullptr}; - EventBits_t full_duplex_tx_overflow_bits_{0}; }; } // namespace esphome::i2s_audio diff --git a/Integrations/ESPHome/components/i2s_audio/microphone/__init__.py b/Integrations/ESPHome/components/i2s_audio/microphone/__init__.py index 89e9991..c217317 100644 --- a/Integrations/ESPHome/components/i2s_audio/microphone/__init__.py +++ b/Integrations/ESPHome/components/i2s_audio/microphone/__init__.py @@ -14,7 +14,6 @@ from .. import ( CONF_ADC_TYPE, - CONF_I2S_AUDIO_ID, CONF_I2S_DIN_PIN, CONF_LEFT, CONF_MONO, @@ -153,8 +152,6 @@ async def to_code(config: ConfigType) -> None: await microphone.register_microphone(var, config) cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) - parent = await cg.get_variable(config[CONF_I2S_AUDIO_ID]) - cg.add(parent.set_din_pin(config[CONF_I2S_DIN_PIN])) cg.add(var.set_pdm(config[CONF_PDM])) if esp32.get_esp32_variant() in PDM_VARIANTS: cg.add(var.set_pdm_dsr(config[CONF_PDM_DSR])) diff --git a/Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 1ff4494..98ef1ad 100644 --- a/Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -94,72 +94,58 @@ void I2SAudioMicrophone::start() { xSemaphoreTake(this->active_listeners_semaphore_, 0); } -bool I2SAudioMicrophone::start_driver_() { - if (this->parent_->is_full_duplex()) { - if (this->pdm_) { - ESP_LOGE(TAG, "Full duplex I2S microphone does not support PDM mode"); - return false; - } - - esp_err_t err; - 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, - .intr_priority = 3, - }; - - i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT; -#if SOC_CLK_APLL_SUPPORTED - if (this->use_apll_) { - clk_src = i2s_clock_src_t::I2S_CLK_SRC_APLL; - } +i2s_clock_src_t I2SAudioMicrophone::get_clock_source_() const { +#ifdef I2S_CLK_SRC_APLL + if (this->use_apll_) { + return I2S_CLK_SRC_APLL; + } #endif + return I2S_CLK_SRC_DEFAULT; +} - i2s_std_gpio_config_t pin_config = this->parent_->get_full_duplex_pin_config(); - pin_config.din = this->din_pin_; - - i2s_std_clk_config_t clk_cfg = { - .sample_rate_hz = this->sample_rate_, - .clk_src = clk_src, - .mclk_multiple = this->mclk_multiple_, - }; - i2s_std_slot_config_t std_slot_cfg = - I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) this->slot_bit_width_, this->slot_mode_); - std_slot_cfg.slot_bit_width = this->slot_bit_width_; - std_slot_cfg.slot_mask = this->std_slot_mask_; - - i2s_std_config_t std_cfg = { - .clk_cfg = clk_cfg, - .slot_cfg = std_slot_cfg, - .gpio_cfg = pin_config, - }; +void I2SAudioMicrophone::build_std_config_(i2s_std_config_t &std_cfg) const { + i2s_std_slot_config_t std_slot_cfg = + I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) this->slot_bit_width_, this->slot_mode_); + std_slot_cfg.slot_bit_width = this->slot_bit_width_; + std_slot_cfg.slot_mask = this->std_slot_mask_; - err = this->parent_->setup_full_duplex_rx_channel(chan_cfg, std_cfg, &this->rx_handle_); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Error setting up full duplex RX channel: %s", esp_err_to_name(err)); - return false; - } + i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config(); + pin_config.din = this->din_pin_; + + std_cfg = { + .clk_cfg = + { + .sample_rate_hz = this->sample_rate_, + .clk_src = this->get_clock_source_(), + .mclk_multiple = this->mclk_multiple_, + }, + .slot_cfg = std_slot_cfg, + .gpio_cfg = pin_config, + }; +} - err = this->parent_->ensure_full_duplex_tx_running(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Error starting full duplex TX clock: %s", esp_err_to_name(err)); - return false; - } +#ifdef USE_I2S_AUDIO_FULL_DUPLEX +bool I2SAudioMicrophone::build_full_duplex_config(i2s_std_config_t &std_cfg) { + if (this->pdm_) { + return false; + } + this->build_std_config_(std_cfg); + return true; +} +#endif - err = i2s_channel_enable(this->rx_handle_); - if (err == ESP_OK || err == ESP_ERR_INVALID_STATE) { - this->parent_->mark_full_duplex_rx_running(); - } else { - ESP_LOGE(TAG, "Enabling full duplex RX failed: %s", esp_err_to_name(err)); +bool I2SAudioMicrophone::start_driver_() { +#ifdef USE_I2S_AUDIO_FULL_DUPLEX + if (this->parent_->is_full_duplex()) { + this->rx_handle_ = this->parent_->acquire_rx_channel(); + if (this->rx_handle_ == nullptr) { + ESP_LOGE(TAG, "Full duplex channel unavailable"); return false; } - this->configure_stream_settings_(); return true; } +#endif if (!this->parent_->try_lock()) { return false; // Waiting for another i2s to return lock @@ -181,18 +167,12 @@ bool I2SAudioMicrophone::start_driver_() { return false; } - 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_std_gpio_config_t pin_config = this->parent_->get_pin_config(); i2s_pdm_rx_clk_config_t clk_cfg = { .sample_rate_hz = this->sample_rate_, - .clk_src = clk_src, + .clk_src = this->get_clock_source_(), .mclk_multiple = this->mclk_multiple_, .dn_sample_mode = this->pdm_dsr_, }; @@ -228,23 +208,8 @@ bool I2SAudioMicrophone::start_driver_() { } else #endif { - i2s_std_clk_config_t clk_cfg = { - .sample_rate_hz = this->sample_rate_, - .clk_src = clk_src, - .mclk_multiple = this->mclk_multiple_, - }; - i2s_std_slot_config_t std_slot_cfg = - I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) this->slot_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, - }; + i2s_std_config_t std_cfg; + this->build_std_config_(std_cfg); /* Initialize the channel */ err = i2s_channel_init_std_mode(this->rx_handle_, &std_cfg); } @@ -276,15 +241,18 @@ void I2SAudioMicrophone::stop_driver_() { // There is no harm continuing to unload the driver if an error is ever returned by the various functions. This // ensures that we stop/unload the driver when it only partially starts. - esp_err_t err; +#ifdef USE_I2S_AUDIO_FULL_DUPLEX if (this->parent_->is_full_duplex()) { + // The parent owns the channel pair, so only hand the RX channel back if (this->rx_handle_ != nullptr) { - this->parent_->release_full_duplex_rx_channel(this->rx_handle_); + this->parent_->release_rx_channel(); this->rx_handle_ = nullptr; } return; } +#endif + esp_err_t err; if (this->rx_handle_ != nullptr) { /* Have to stop the channel before deleting it */ err = i2s_channel_disable(this->rx_handle_); diff --git a/Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.h b/Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.h index 5cdb526..972c986 100644 --- a/Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -33,7 +33,16 @@ class I2SAudioMicrophone final : public I2SAudioIn, public microphone::Microphon void set_pdm_dsr(i2s_pdm_dsr_t pdm_dsr) { this->pdm_dsr_ = pdm_dsr; } #endif +#ifdef USE_I2S_AUDIO_FULL_DUPLEX + bool build_full_duplex_config(i2s_std_config_t &std_cfg) override; +#endif + protected: + /// @brief Builds the standard (non-PDM) mode configuration from the configured settings. + void build_std_config_(i2s_std_config_t &std_cfg) const; + + i2s_clock_src_t get_clock_source_() const; + /// @brief Starts the I2S driver. Updates the ``audio_stream_info_`` member variable with the current setttings. /// @return True if succesful, false otherwise bool start_driver_(); diff --git a/Integrations/ESPHome/components/i2s_audio/speaker/__init__.py b/Integrations/ESPHome/components/i2s_audio/speaker/__init__.py index 7ba7a5e..0465ab5 100644 --- a/Integrations/ESPHome/components/i2s_audio/speaker/__init__.py +++ b/Integrations/ESPHome/components/i2s_audio/speaker/__init__.py @@ -17,7 +17,7 @@ from esphome.types import ConfigType from .. import ( - CONF_I2S_AUDIO_ID, + CONF_I2S_COMM_FMT, CONF_I2S_DOUT_PIN, CONF_I2S_MODE, CONF_LEFT, @@ -25,6 +25,7 @@ CONF_MONO, CONF_PRIMARY, CONF_RIGHT, + CONF_SPDIF_MODE, CONF_STEREO, CONF_USE_APLL, I2SAudioOut, @@ -44,8 +45,6 @@ I2SAudioSpeaker = i2s_audio_ns.class_("I2SAudioSpeaker", I2SAudioSpeakerBase) CONF_DAC_TYPE = "dac_type" -CONF_I2S_COMM_FMT = "i2s_comm_fmt" -CONF_SPDIF_MODE = "spdif_mode" I2SAudioSpeakerBase = i2s_audio_ns.class_( "I2SAudioSpeakerBase", cg.Component, speaker.Speaker, I2SAudioOut @@ -248,8 +247,6 @@ async def to_code(config: ConfigType) -> None: await speaker.register_speaker(var, config) cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) - parent = await cg.get_variable(config[CONF_I2S_AUDIO_ID]) - cg.add(parent.set_dout_pin(config[CONF_I2S_DOUT_PIN])) is_spdif = config.get(CONF_SPDIF_MODE, False) if is_spdif: diff --git a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 62d4538..fb08a06 100644 --- a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -130,9 +130,11 @@ void I2SAudioSpeakerBase::loop() { break; } - if (this->start_i2s_driver(this->audio_stream_info_) != ESP_OK) { - ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); - this->status_momentary_error("driver-failure", 1000); + if (const esp_err_t err = this->start_i2s_driver(this->audio_stream_info_); err != ESP_OK) { + if (err != ESP_ERR_NOT_FINISHED) { + ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); + this->status_momentary_error("driver-failure", 1000); + } break; } @@ -267,32 +269,32 @@ void I2SAudioSpeakerBase::stop_(bool wait_on_empty) { esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg, size_t event_queue_size) { - esp_err_t err = ESP_OK; - if (this->parent_->is_full_duplex()) { - err = this->parent_->setup_full_duplex_tx_channel(chan_cfg, std_cfg, &this->tx_handle_); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Full duplex TX setup failed: %s", esp_err_to_name(err)); - this->tx_handle_ = nullptr; - return err; - } - } else { - err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL); - if (err != ESP_OK) { - ESP_LOGE(TAG, "I2S channel allocation failed: %s", esp_err_to_name(err)); - this->parent_->unlock(); - return err; - } + esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL); + if (err != ESP_OK) { + ESP_LOGE(TAG, "I2S channel allocation failed: %s", esp_err_to_name(err)); + this->parent_->unlock(); + return err; + } - err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to initialize I2S channel"); - i2s_del_channel(this->tx_handle_); - this->tx_handle_ = nullptr; - this->parent_->unlock(); - return err; - } + err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize I2S channel"); + i2s_del_channel(this->tx_handle_); + this->tx_handle_ = nullptr; + this->parent_->unlock(); + return err; } + err = this->prepare_event_queues_(event_queue_size); + if (err != ESP_OK) { + i2s_del_channel(this->tx_handle_); + this->tx_handle_ = nullptr; + this->parent_->unlock(); + } + return err; +} + +esp_err_t I2SAudioSpeakerBase::prepare_event_queues_(size_t event_queue_size) { if (this->i2s_event_queue_ == nullptr) { this->i2s_event_queue_ = xQueueCreate(event_queue_size, sizeof(int64_t)); } else { @@ -310,27 +312,47 @@ esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_c if (this->i2s_event_queue_ == nullptr || this->write_records_queue_ == nullptr) { ESP_LOGE(TAG, "Failed to allocate I2S event queue(s)"); - if (this->parent_->is_full_duplex()) { - this->parent_->release_full_duplex_tx_channel(this->tx_handle_); - } else { - i2s_del_channel(this->tx_handle_); - this->parent_->unlock(); - } - this->tx_handle_ = nullptr; return ESP_ERR_NO_MEM; } return ESP_OK; } +#ifdef USE_I2S_AUDIO_FULL_DUPLEX +esp_err_t I2SAudioSpeakerBase::acquire_full_duplex_channel_(size_t event_queue_size) { + if (this->parent_->is_tx_in_use()) { + // Another speaker on the bus is playing; loop() retries until it releases the channel + ESP_LOGV(TAG, "Full duplex channel busy"); + return ESP_ERR_NOT_FINISHED; + } + this->tx_handle_ = this->parent_->acquire_tx_channel(); + if (this->tx_handle_ == nullptr) { + ESP_LOGE(TAG, "Full duplex channel unavailable"); + return ESP_ERR_INVALID_STATE; + } + + esp_err_t err = this->prepare_event_queues_(event_queue_size); + if (err != ESP_OK) { + this->parent_->release_tx_channel(); + this->tx_handle_ = nullptr; + } + return err; +} +#endif + void I2SAudioSpeakerBase::stop_i2s_driver_() { - if (this->tx_handle_ != nullptr) { - if (this->parent_->is_full_duplex()) { - this->parent_->release_full_duplex_tx_channel(this->tx_handle_); +#ifdef USE_I2S_AUDIO_FULL_DUPLEX + if (this->parent_->is_full_duplex()) { + // The parent owns the channel pair and its pins, so only hand the TX channel back + if (this->tx_handle_ != nullptr) { + this->parent_->release_tx_channel(); this->tx_handle_ = nullptr; - return; } + return; + } +#endif + if (this->tx_handle_ != nullptr) { i2s_channel_disable(this->tx_handle_); i2s_del_channel(this->tx_handle_); this->tx_handle_ = nullptr; diff --git a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.h b/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.h index adb6ca5..5f60e24 100644 --- a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -122,6 +122,18 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public esp_err_t init_i2s_channel_(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg, size_t event_queue_size); + /// @brief Creates the lockstep queues, or empties them if they already exist. + /// @param event_queue_size Size of each queue + /// @return ESP_OK if successful, or ESP_ERR_NO_MEM + esp_err_t prepare_event_queues_(size_t event_queue_size); + +#ifdef USE_I2S_AUDIO_FULL_DUPLEX + /// @brief Takes the TX channel of the parent's full duplex pair and prepares the event queues. + /// @param event_queue_size Size of the event queue + /// @return ESP_OK if successful, or an error code. On failure, hands the channel back to the parent. + esp_err_t acquire_full_duplex_channel_(size_t event_queue_size); +#endif + /// @brief Stops the I2S driver and unlocks the I2S port void stop_i2s_driver_(); diff --git a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp b/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp index a7993e6..5e0624b 100644 --- a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp +++ b/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp @@ -76,7 +76,6 @@ void I2SAudioSpeaker::dump_config() { void I2SAudioSpeaker::run_speaker_task() { xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING); - const bool full_duplex = this->parent_->is_full_duplex(); const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT; // Ensure ring buffer duration is at least the duration of all DMA buffers const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this->buffer_duration_ms_); @@ -99,18 +98,12 @@ void I2SAudioSpeaker::run_speaker_task() { // the L1 cache (e.g. ESP32-P4), rounds the buffer to the cache line. Read the size the driver actually // allocated so preload, silence padding, and the write/event lockstep all match it exactly. The channel is // in the READY state here because start_i2s_driver() initialized it before this task was created. - size_t dma_buffer_bytes = 0; + size_t dma_buffer_bytes; i2s_chan_info_t chan_info; if (i2s_channel_get_info(this->tx_handle_, &chan_info) == ESP_OK && chan_info.total_dma_buf_size > 0) { - // total_dma_buf_size spans all descriptors and is an exact multiple of their count. In full duplex the - // microphone may have allocated the shared channel, so its descriptor count applies instead. - const size_t dma_desc_num = - full_duplex ? this->parent_->get_full_duplex_dma_desc_num() : static_cast(DMA_BUFFERS_COUNT); - if (dma_desc_num > 0) { - dma_buffer_bytes = chan_info.total_dma_buf_size / dma_desc_num; - } - } - if (dma_buffer_bytes == 0) { + // total_dma_buf_size spans all DMA_BUFFERS_COUNT descriptors and is an exact multiple of the count. + dma_buffer_bytes = chan_info.total_dma_buf_size / DMA_BUFFERS_COUNT; + } else { // Should not happen for a READY channel; fall back to the requested size. dma_buffer_bytes = this->output_stream_info_.frames_to_bytes(dma_buffer_frames(this->output_stream_info_)); } @@ -141,18 +134,7 @@ void I2SAudioSpeaker::run_speaker_task() { } } - if (successful_setup && full_duplex) { - // The bus owns the shared TX channel and its on_sent callback, since TX may already be clocking the - // microphone. It seeds the write records to match wherever the DMA ring stands. - if (this->parent_->attach_full_duplex_speaker(this->i2s_event_queue_, this->write_records_queue_, - this->event_group_, - SpeakerEventGroupBits::ERR_DROPPED_EVENT | - SpeakerEventGroupBits::COMMAND_STOP, - silence_buffer, dma_buffer_bytes) != ESP_OK) { - ESP_LOGV(TAG, "Failed to join full duplex TX"); - successful_setup = false; - } - } else if (successful_setup) { + if (successful_setup) { // Preload every DMA descriptor with silence and push a matching zero-real-frames record per buffer. // This guarantees that every on_sent event has a corresponding write record from the start, so // ``i2s_event_queue_`` and ``write_records_queue_`` stay in lockstep for the entire task lifetime. @@ -175,7 +157,7 @@ void I2SAudioSpeaker::run_speaker_task() { } } - if (successful_setup && !full_duplex) { + if (successful_setup) { // Register the on_sent callback BEFORE enabling the channel so the very first transmitted buffer // generates a queued event that pairs with the first preloaded silence record. const i2s_event_callbacks_t callbacks = {.on_sent = i2s_on_sent_cb}; @@ -363,11 +345,6 @@ void I2SAudioSpeaker::run_speaker_task() { } } - if (full_duplex) { - // TX keeps running for the microphone; stop routing its events to this task - this->parent_->detach_full_duplex_tx_event_queue(this->i2s_event_queue_); - } - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING); audio_source.reset(); @@ -420,16 +397,51 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream } #endif // USE_ESP32_VARIANT_ESP32 - // Full duplex shares the bus with the microphone instead of taking it exclusively - if (!this->parent_->is_full_duplex() && !this->parent_->try_lock()) { +#ifdef USE_I2S_AUDIO_FULL_DUPLEX + if (this->parent_->is_full_duplex()) { + // The parent set up the channel once from the configured format, so the stream must produce exactly that + const audio::AudioStreamInfo required = this->full_duplex_stream_info_(); + if (this->output_stream_info_ != required) { + ESP_LOGE(TAG, "Full duplex requires %u-bit, %u channel audio at %" PRIu32 " Hz", + (unsigned) required.get_bits_per_sample(), (unsigned) required.get_channels(), + required.get_sample_rate()); + return ESP_ERR_NOT_SUPPORTED; + } + // The speaker task will enable the channel after preloading. + return this->acquire_full_duplex_channel_(I2S_EVENT_QUEUE_COUNT); + } +#endif // USE_I2S_AUDIO_FULL_DUPLEX + + if (!this->parent_->try_lock()) { ESP_LOGE(TAG, "Parent bus is busy"); return ESP_ERR_INVALID_STATE; } + i2s_chan_config_t chan_cfg; + i2s_std_config_t std_cfg; + this->build_i2s_config_(this->output_stream_info_, chan_cfg, std_cfg); + + // The speaker task will enable the channel after preloading. + return this->init_i2s_channel_(chan_cfg, std_cfg, I2S_EVENT_QUEUE_COUNT); +} + +#ifdef USE_I2S_AUDIO_FULL_DUPLEX +audio::AudioStreamInfo I2SAudioSpeaker::full_duplex_stream_info_() const { + return audio::AudioStreamInfo(static_cast(this->slot_bit_width_), + this->slot_mode_ == I2S_SLOT_MODE_STEREO ? 2 : 1, this->sample_rate_); +} + +bool I2SAudioSpeaker::build_full_duplex_config(i2s_chan_config_t &chan_cfg, i2s_std_config_t &std_cfg) { + this->build_i2s_config_(this->full_duplex_stream_info_(), chan_cfg, std_cfg); + return true; +} +#endif // USE_I2S_AUDIO_FULL_DUPLEX + +void I2SAudioSpeaker::build_i2s_config_(const audio::AudioStreamInfo &output_stream_info, i2s_chan_config_t &chan_cfg, + i2s_std_config_t &std_cfg) const { // The DMA buffers hold output-format (post-narrowing) samples, so size them from the output stream info. - uint32_t dma_buffer_length = dma_buffer_frames(this->output_stream_info_); + uint32_t dma_buffer_length = dma_buffer_frames(output_stream_info); - i2s_role_t i2s_role = this->i2s_role_; i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT; #if SOC_CLK_APLL_SUPPORTED @@ -442,9 +454,9 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream ESP_LOGV(TAG, "I2S DMA config: %zu buffers x %lu frames", (size_t) DMA_BUFFERS_COUNT, (unsigned long) dma_buffer_length); - i2s_chan_config_t chan_cfg = { + chan_cfg = { .id = this->parent_->get_port(), - .role = i2s_role, + .role = this->i2s_role_, .dma_desc_num = DMA_BUFFERS_COUNT, .dma_frame_num = dma_buffer_length, .auto_clear = true, @@ -453,22 +465,22 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream // Build standard I2S clock/slot/gpio configuration i2s_std_clk_config_t clk_cfg = { - .sample_rate_hz = audio_stream_info.get_sample_rate(), + .sample_rate_hz = output_stream_info.get_sample_rate(), .clk_src = clk_src, .mclk_multiple = this->mclk_multiple_, }; 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) { + if (output_stream_info.get_channels() == 1) { slot_mode = I2S_SLOT_MODE_MONO; - } else if (audio_stream_info.get_channels() == 2) { + } else if (output_stream_info.get_channels() == 2) { slot_mode = I2S_SLOT_MODE_STEREO; slot_mask = I2S_STD_SLOT_BOTH; } // Configure the data bit width from the output (post-narrowing) format, which is what is clocked out. - const i2s_data_bit_width_t data_bit_width = (i2s_data_bit_width_t) this->output_stream_info_.get_bits_per_sample(); + const i2s_data_bit_width_t data_bit_width = (i2s_data_bit_width_t) output_stream_info.get_bits_per_sample(); i2s_std_slot_config_t slot_cfg; switch (this->i2s_comm_fmt_) { case I2SCommFmt::PCM: @@ -504,20 +516,11 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream i2s_std_gpio_config_t gpio_cfg = this->parent_->get_pin_config(); gpio_cfg.dout = this->dout_pin_; - i2s_std_config_t std_cfg = { + std_cfg = { .clk_cfg = clk_cfg, .slot_cfg = slot_cfg, .gpio_cfg = gpio_cfg, }; - - esp_err_t err = this->init_i2s_channel_(chan_cfg, std_cfg, I2S_EVENT_QUEUE_COUNT); - if (err != ESP_OK) { - return err; - } - - // The speaker task will enable the channel after preloading. - - return ESP_OK; } } // namespace esphome::i2s_audio diff --git a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.h b/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.h index 4b52dcd..cfa5bd8 100644 --- a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.h +++ b/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.h @@ -20,10 +20,24 @@ class I2SAudioSpeaker final : public I2SAudioSpeakerBase { void set_i2s_comm_fmt(I2SCommFmt fmt) { this->i2s_comm_fmt_ = fmt; } +#ifdef USE_I2S_AUDIO_FULL_DUPLEX + bool build_full_duplex_config(i2s_chan_config_t &chan_cfg, i2s_std_config_t &std_cfg) override; +#endif + protected: void run_speaker_task() override; esp_err_t start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) override; + /// @brief Builds the channel and standard mode configuration for the given output format. + /// @param output_stream_info Format clocked out of the I2S peripheral + void build_i2s_config_(const audio::AudioStreamInfo &output_stream_info, i2s_chan_config_t &chan_cfg, + i2s_std_config_t &std_cfg) const; + +#ifdef USE_I2S_AUDIO_FULL_DUPLEX + /// @brief The output format of a full duplex bus, fixed by the configuration since the channel is set up once. + audio::AudioStreamInfo full_duplex_stream_info_() const; +#endif + I2SCommFmt i2s_comm_fmt_{I2SCommFmt::STANDARD}; }; From e436ba627d0530e25b06e69f8ca69a0955adae05 Mon Sep 17 00:00:00 2001 From: Justin Bunton Date: Thu, 8 Oct 2026 13:36:52 -0400 Subject: [PATCH 2/3] Use esphome/esphome#19959 directly and build on ESPHome beta Drop the vendored i2s_audio and load full duplex straight from #19959, pinned to its head commit. The PR builds on ESPHome 2026.10, so: - min_version is now 2026.10.0b1, giving older ESPHome a clear error - build-beta.yml builds with ESPHome beta - CI and weekly checks skip stable until 2026.10.0 ships build.yml (main) stays on stable, so stable firmware can't be published on a beta ESPHome. Once #19959 ships in a release, remove the external_components block and restore stable in the workflows. --- .github/workflows/build-beta.yml | 3 +- .github/workflows/ci.yml | 2 +- .github/workflows/weekly.yml | 2 +- Integrations/ESPHome/Core.yaml | 10 +- .../ESPHome/components/i2s_audio/README.md | 14 - .../ESPHome/components/i2s_audio/__init__.py | 383 ------------- .../components/i2s_audio/i2s_audio.cpp | 101 ---- .../ESPHome/components/i2s_audio/i2s_audio.h | 131 ----- .../i2s_audio/media_player/__init__.py | 7 - .../i2s_audio/microphone/__init__.py | 159 ------ .../microphone/i2s_audio_microphone.cpp | 471 ---------------- .../microphone/i2s_audio_microphone.h | 85 --- .../components/i2s_audio/speaker/__init__.py | 274 --------- .../i2s_audio/speaker/i2s_audio_spdif.cpp | 504 ----------------- .../i2s_audio/speaker/i2s_audio_spdif.h | 34 -- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 427 -------------- .../i2s_audio/speaker/i2s_audio_speaker.h | 184 ------ .../speaker/i2s_audio_speaker_standard.cpp | 528 ------------------ .../speaker/i2s_audio_speaker_standard.h | 46 -- .../i2s_audio/speaker/spdif_encoder.cpp | 492 ---------------- .../i2s_audio/speaker/spdif_encoder.h | 148 ----- 21 files changed, 10 insertions(+), 3995 deletions(-) delete mode 100644 Integrations/ESPHome/components/i2s_audio/README.md delete mode 100644 Integrations/ESPHome/components/i2s_audio/__init__.py delete mode 100644 Integrations/ESPHome/components/i2s_audio/i2s_audio.cpp delete mode 100644 Integrations/ESPHome/components/i2s_audio/i2s_audio.h delete mode 100644 Integrations/ESPHome/components/i2s_audio/media_player/__init__.py delete mode 100644 Integrations/ESPHome/components/i2s_audio/microphone/__init__.py delete mode 100644 Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.cpp delete mode 100644 Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.h delete mode 100644 Integrations/ESPHome/components/i2s_audio/speaker/__init__.py delete mode 100644 Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_spdif.cpp delete mode 100644 Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_spdif.h delete mode 100644 Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.cpp delete mode 100644 Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.h delete mode 100644 Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp delete mode 100644 Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.h delete mode 100644 Integrations/ESPHome/components/i2s_audio/speaker/spdif_encoder.cpp delete mode 100644 Integrations/ESPHome/components/i2s_audio/speaker/spdif_encoder.h diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml index 759795e..612760f 100644 --- a/.github/workflows/build-beta.yml +++ b/.github/workflows/build-beta.yml @@ -47,7 +47,8 @@ jobs: uses: esphome/workflows/.github/workflows/build.yml@9f6577fd37b5cf773ab1b9be929714a0dcd15661 # 2026.7.0 with: files: ${{ matrix.yaml }} - esphome-version: stable + # Full duplex (esphome/esphome#19959) needs 2026.10; back to stable once it ships + esphome-version: beta combined-name: ${{ matrix.name }} release-version: ${{ needs.version.outputs.v }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72eed2b..e6375b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: with: yaml-files: | Integrations/ESPHome/CAST_PRO-1.yaml + # stable returns with 2026.10.0; full duplex needs it esphome-versions: | - stable beta dev diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index c616416..c92d1c5 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -15,6 +15,6 @@ jobs: yaml-files: | Integrations/ESPHome/CAST_PRO-1.yaml # dev excluded until the TemplateSelect .state incompatibility is fixed + # stable returns with 2026.10.0; full duplex needs it esphome-versions: | - stable beta diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index f8960ef..24442b6 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -9,18 +9,20 @@ substitutions: packages: wizmote: !include wizmote.yaml -# Full-duplex i2s_audio; see components/i2s_audio/README.md +# Full-duplex i2s_audio from esphome/esphome#19959; drop once it ships external_components: - source: - type: local - path: components + type: git + url: https://github.com/esphome/esphome + ref: 04d475970e1835ba79ace1e5303749aac833e191 + path: esphome/components components: [i2s_audio] esphome: friendly_name: Apollo CAST_PRO-1 comment: Apollo CAST_PRO-1 name_add_mac_suffix: true - min_version: 2026.8.0 + min_version: 2026.10.0b1 platformio_options: board_build.flash_mode: dio # List form so wizmote.yaml's on_boot entries merge diff --git a/Integrations/ESPHome/components/i2s_audio/README.md b/Integrations/ESPHome/components/i2s_audio/README.md deleted file mode 100644 index ef6d441..0000000 --- a/Integrations/ESPHome/components/i2s_audio/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# i2s_audio (full duplex) - -ESPHome 2026.9.1's `i2s_audio` with full duplex support from esphome/esphome#19959 -(head `04d4759`), so the PCM5122 DAC and PCM1808 ADC can share one I2S port -(common WS/BCLK/MCLK, separate DOUT/DIN). - -Only the PR's own changes are applied, on top of 2026.9.1. The PR is built on -ESPHome dev, and dev's other `i2s_audio` changes need a newer esp-audio-libs and -`audio_dac` than 2026.9.1 has. One PR hunk is left out: a guard in the speaker's -`setup()` that skips parking the data pin on a full duplex bus. 2026.9.1 doesn't -park the pin there, so there's nothing to skip. - -Delete this folder and the `external_components` block in Core.yaml once #19959 -ships in an ESPHome release. diff --git a/Integrations/ESPHome/components/i2s_audio/__init__.py b/Integrations/ESPHome/components/i2s_audio/__init__.py deleted file mode 100644 index 2361eb2..0000000 --- a/Integrations/ESPHome/components/i2s_audio/__init__.py +++ /dev/null @@ -1,383 +0,0 @@ -from dataclasses import dataclass, field - -from esphome import pins -import esphome.codegen as cg -from esphome.components.esp32 import ( - add_idf_sdkconfig_option, - get_esp32_variant, - include_builtin_idf_component, -) -from esphome.components.esp32.const import ( - VARIANT_ESP32, - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32H2, - VARIANT_ESP32P4, - VARIANT_ESP32S2, - VARIANT_ESP32S3, -) -from esphome.config_helpers import filter_source_files_from_defines -import esphome.config_validation as cv -from esphome.const import ( - CONF_BITS_PER_SAMPLE, - CONF_CHANNEL, - CONF_ID, - CONF_MICROPHONE, - CONF_PLATFORM, - CONF_SAMPLE_RATE, - CONF_SPEAKER, -) -from esphome.core import CORE -from esphome.cpp_generator import MockObj, MockObjClass -import esphome.final_validate as fv -from esphome.types import ConfigType - -CODEOWNERS = ["@jesserockz"] -DEPENDENCIES = ["esp32"] -MULTI_CONF = True - -CONF_PDM = "pdm" -CONF_PDM_DSR = "pdm_dsr" -CONF_ADC_TYPE = "adc_type" - -CONF_I2S_DOUT_PIN = "i2s_dout_pin" -CONF_I2S_DIN_PIN = "i2s_din_pin" -CONF_I2S_MCLK_PIN = "i2s_mclk_pin" -CONF_I2S_BCLK_PIN = "i2s_bclk_pin" -CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin" - -CONF_I2S_AUDIO = "i2s_audio" -CONF_I2S_AUDIO_ID = "i2s_audio_id" -CONF_FULL_DUPLEX = "full_duplex" -CONF_I2S_COMM_FMT = "i2s_comm_fmt" -CONF_SPDIF_MODE = "spdif_mode" - -CONF_I2S_MODE = "i2s_mode" -CONF_PRIMARY = "primary" -CONF_SECONDARY = "secondary" - -CONF_USE_APLL = "use_apll" -CONF_BITS_PER_CHANNEL = "bits_per_channel" -CONF_MCLK_MULTIPLE = "mclk_multiple" -CONF_MONO = "mono" -CONF_LEFT = "left" -CONF_RIGHT = "right" -CONF_STEREO = "stereo" -CONF_BOTH = "both" - -i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio") -I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component) -I2SAudioBase = i2s_audio_ns.class_( - "I2SAudioBase", cg.Parented.template(I2SAudioComponent) -) -I2SAudioIn = i2s_audio_ns.class_("I2SAudioIn", I2SAudioBase) -I2SAudioOut = i2s_audio_ns.class_("I2SAudioOut", I2SAudioBase) - -i2s_mode_t = cg.global_ns.enum("i2s_mode_t") -I2S_MODE_OPTIONS = { - CONF_PRIMARY: i2s_mode_t.I2S_MODE_MASTER, # NOLINT - 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 (SOC_I2S_NUM) -I2S_PORTS = { - VARIANT_ESP32: 2, - VARIANT_ESP32C3: 1, - VARIANT_ESP32C5: 1, - VARIANT_ESP32C6: 1, - VARIANT_ESP32C61: 1, - VARIANT_ESP32H2: 1, - VARIANT_ESP32P4: 3, - VARIANT_ESP32S2: 1, - VARIANT_ESP32S3: 2, -} - -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, # 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") -I2S_BITS_PER_SAMPLE = { - 8: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_8BIT, - 16: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_16BIT, - 24: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_24BIT, - 32: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_32BIT, -} - -i2s_bits_per_chan_t = cg.global_ns.enum("i2s_bits_per_chan_t") -I2S_BITS_PER_CHANNEL = { - "default": i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_DEFAULT, - 8: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_8BIT, - 16: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_16BIT, - 24: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_24BIT, - 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, -} - -i2s_mclk_multiple_t = cg.global_ns.enum("i2s_mclk_multiple_t") -I2S_MCLK_MULTIPLE = { - 128: i2s_mclk_multiple_t.I2S_MCLK_MULTIPLE_128, - 256: i2s_mclk_multiple_t.I2S_MCLK_MULTIPLE_256, - 384: i2s_mclk_multiple_t.I2S_MCLK_MULTIPLE_384, - 512: i2s_mclk_multiple_t.I2S_MCLK_MULTIPLE_512, -} - -_validate_bits = cv.float_with_unit("bits", "bit") - - -def validate_mclk_divisible_by_3(config: ConfigType) -> ConfigType: - if config[CONF_BITS_PER_SAMPLE] == 24 and config[CONF_MCLK_MULTIPLE] % 3 != 0: - raise cv.Invalid( - f"{CONF_MCLK_MULTIPLE} must be divisible by 3 when bits per sample is 24" - ) - return config - - -def i2s_audio_component_schema( - class_: MockObjClass, - *, - default_sample_rate: int, - default_channel: str, - default_bits_per_sample: str, -) -> cv.Schema: - return cv.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.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.int_, cv.one_of(*I2S_BITS_PER_SAMPLE) - ), - 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_MCLK_MULTIPLE, default=256): cv.one_of(*I2S_MCLK_MULTIPLE), - } - ) - - -async def register_i2s_audio_component(var: MockObj, config: ConfigType) -> None: - await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) - 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_SAMPLE]])) - cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) - cg.add(var.set_use_apll(config[CONF_USE_APLL])) - cg.add(var.set_mclk_multiple(I2S_MCLK_MULTIPLE[config[CONF_MCLK_MULTIPLE]])) - - if str(config[CONF_I2S_AUDIO_ID]) in _get_data().full_duplex_buses: - parent = await cg.get_variable(config[CONF_I2S_AUDIO_ID]) - if config[CONF_ID].type.inherits_from(I2SAudioIn): - cg.add(parent.set_audio_in(var)) - else: - cg.add(parent.set_audio_out(var)) - - -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(I2SAudioComponent), - cv.Optional(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_FULL_DUPLEX, default=False): cv.boolean, - }, -) - - -@dataclass -class I2SAudioData: - """I2S audio component state stored in CORE.data.""" - - port_map: dict[str, int] = field(default_factory=dict) - full_duplex_buses: set[str] = field(default_factory=set) - - -def _get_data() -> I2SAudioData: - if CONF_I2S_AUDIO not in CORE.data: - CORE.data[CONF_I2S_AUDIO] = I2SAudioData() - return CORE.data[CONF_I2S_AUDIO] - - -def _assign_ports() -> None: - """Assign I2S port numbers, prioritizing instances with microphone children. - - Microphones (especially PDM) require port 0 on most ESP32 variants. - This runs once and stores the mapping in CORE.data. - """ - data = _get_data() - if data.port_map: - return - - full_config = fv.full_config.get() - i2s_configs = full_config[CONF_I2S_AUDIO] - - # Find i2s_audio instances with microphones that require port 0 - # (PDM and internal ADC only work on I2S port 0) - port0_parent_id = None - for mic_config in full_config.get("microphone", []): - if CONF_I2S_AUDIO_ID not in mic_config: - continue - if mic_config.get(CONF_PDM) or mic_config.get(CONF_ADC_TYPE) == "internal": - if port0_parent_id is not None: - raise cv.Invalid( - "Only one PDM/ADC microphone is supported (requires I2S port 0)" - ) - port0_parent_id = str(mic_config[CONF_I2S_AUDIO_ID]) - - # Assign ports: port 0 parent first (if any), rest get sequential - next_port = 0 - if port0_parent_id is not None: - data.port_map[port0_parent_id] = next_port - next_port += 1 - for config in i2s_configs: - config_id = str(config[CONF_ID]) - if config_id != port0_parent_id: - data.port_map[config_id] = next_port - next_port += 1 - - -def _bus_devices(full_config: ConfigType, domain: str, bus_id: str) -> list[ConfigType]: - return [ - device - for device in full_config.get(domain, []) - if device.get(CONF_PLATFORM) == CONF_I2S_AUDIO - and str(device[CONF_I2S_AUDIO_ID]) == bus_id - ] - - -def _validate_full_duplex(full_config: ConfigType, bus_id: str) -> None: - """Check that a full duplex bus has one microphone and speakers that can share its clocks and TX channel.""" - microphones = _bus_devices(full_config, CONF_MICROPHONE, bus_id) - speakers = _bus_devices(full_config, CONF_SPEAKER, bus_id) - if len(microphones) != 1 or not speakers: - raise cv.Invalid( - f"'{CONF_FULL_DUPLEX}' requires exactly one i2s_audio microphone and at least one " - f"i2s_audio speaker on bus '{bus_id}'" - ) - microphone = microphones[0] - if microphone.get(CONF_PDM): - raise cv.Invalid(f"A PDM microphone cannot use a '{CONF_FULL_DUPLEX}' bus") - for speaker in speakers: - if speaker.get(CONF_SPDIF_MODE): - raise cv.Invalid(f"An SPDIF speaker cannot use a '{CONF_FULL_DUPLEX}' bus") - # Both directions run from the same bit and word clocks - for key in ( - CONF_SAMPLE_RATE, - CONF_BITS_PER_SAMPLE, - CONF_I2S_MODE, - CONF_USE_APLL, - CONF_MCLK_MULTIPLE, - ): - if microphone[key] != speaker[key]: - raise cv.Invalid( - f"The microphone and speaker '{speaker[CONF_ID]}' on '{CONF_FULL_DUPLEX}' bus " - f"'{bus_id}' must use the same '{key}'" - ) - # The TX channel is set up once from one speaker's configuration, and the speakers take turns using it - first = speakers[0] - for speaker in speakers[1:]: - for key in (CONF_I2S_DOUT_PIN, CONF_CHANNEL, CONF_I2S_COMM_FMT): - if speaker.get(key) != first.get(key): - raise cv.Invalid( - f"The speakers on '{CONF_FULL_DUPLEX}' bus '{bus_id}' must use the same '{key}'" - ) - - -def _final_validate(_: ConfigType) -> None: - full_config = fv.full_config.get() - i2s_audio_configs = full_config[CONF_I2S_AUDIO] - variant = get_esp32_variant() - if variant not in I2S_PORTS: - raise cv.Invalid(f"Unsupported variant {variant}") - if len(i2s_audio_configs) > I2S_PORTS[variant]: - raise cv.Invalid( - f"Only {I2S_PORTS[variant]} I2S audio ports are supported on {variant}" - ) - _assign_ports() - - data = _get_data() - for config in i2s_audio_configs: - if config[CONF_FULL_DUPLEX]: - bus_id = str(config[CONF_ID]) - _validate_full_duplex(full_config, bus_id) - data.full_duplex_buses.add(bus_id) - - -FINAL_VALIDATE_SCHEMA = _final_validate - - -async def to_code(config: ConfigType) -> None: - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - - # Assign I2S port from _final_validate computed mapping - data = _get_data() - if (port := data.port_map.get(str(config[CONF_ID]))) is None: - raise ValueError(f"No I2S port assigned for {config[CONF_ID]}") - cg.add(var.set_port(port)) - - # Re-enable ESP-IDF's I2S driver (excluded by default to save compile time) - include_builtin_idf_component("esp_driver_i2s") - - # Helps avoid callbacks being skipped due to processor load - add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True) - - if CONF_I2S_LRCLK_PIN in config: - cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) - if CONF_I2S_BCLK_PIN in config: - cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) - if CONF_I2S_MCLK_PIN in config: - cg.add(var.set_mclk_pin(config[CONF_I2S_MCLK_PIN])) - if config[CONF_FULL_DUPLEX]: - cg.add_define("USE_I2S_AUDIO_FULL_DUPLEX") - - -FILTER_SOURCE_FILES = filter_source_files_from_defines( - {"i2s_audio.cpp": "USE_I2S_AUDIO_FULL_DUPLEX"} -) diff --git a/Integrations/ESPHome/components/i2s_audio/i2s_audio.cpp b/Integrations/ESPHome/components/i2s_audio/i2s_audio.cpp deleted file mode 100644 index e8b4edb..0000000 --- a/Integrations/ESPHome/components/i2s_audio/i2s_audio.cpp +++ /dev/null @@ -1,101 +0,0 @@ -#include "i2s_audio.h" - -#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_FULL_DUPLEX) - -#include "esphome/core/log.h" - -namespace esphome::i2s_audio { - -static const char *const TAG = "i2s_audio"; - -void I2SAudioComponent::setup() { - if (!this->is_full_duplex()) - return; - - // ESP-IDF only shares the bit and word clocks between channels allocated in the same call, and deleting either - // channel ends the pairing. Allocate both once here and keep them for the lifetime of the device. - i2s_chan_config_t chan_cfg{}; - i2s_std_config_t tx_cfg{}; - i2s_std_config_t rx_cfg{}; - if (!this->audio_out_->build_full_duplex_config(chan_cfg, tx_cfg) || - !this->audio_in_->build_full_duplex_config(rx_cfg)) { - ESP_LOGE(TAG, "Microphone or speaker does not support full duplex"); - this->mark_failed(); - return; - } - - esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, &this->rx_handle_); - if (err == ESP_OK) { - // The first initialized channel drives the shared clocks. Use RX so the microphone runs on its own. - err = i2s_channel_init_std_mode(this->rx_handle_, &rx_cfg); - } - if (err == ESP_OK) { - err = i2s_channel_init_std_mode(this->tx_handle_, &tx_cfg); - } - if (err != ESP_OK) { - ESP_LOGE(TAG, "Full duplex channel setup failed: %s", esp_err_to_name(err)); - if (this->tx_handle_ != nullptr) { - i2s_del_channel(this->tx_handle_); - this->tx_handle_ = nullptr; - } - if (this->rx_handle_ != nullptr) { - i2s_del_channel(this->rx_handle_); - this->rx_handle_ = nullptr; - } - this->mark_failed(); - } -} - -i2s_chan_handle_t I2SAudioComponent::acquire_rx_channel() { - if (this->rx_handle_ == nullptr) - return nullptr; - this->rx_in_use_ = true; - if (!this->update_rx_channel_()) { - this->rx_in_use_ = false; - return nullptr; - } - return this->rx_handle_; -} - -void I2SAudioComponent::release_rx_channel() { - this->rx_in_use_ = false; - this->update_rx_channel_(); -} - -i2s_chan_handle_t I2SAudioComponent::acquire_tx_channel() { - // Speakers sharing the bus take turns; a second one must not get the channel while the first is playing - if (this->tx_handle_ == nullptr || this->tx_in_use_) - return nullptr; - this->tx_in_use_ = true; - if (!this->update_rx_channel_()) { - this->tx_in_use_ = false; - return nullptr; - } - return this->tx_handle_; -} - -void I2SAudioComponent::release_tx_channel() { - // The speaker task may have left the channel disabled already, so an invalid state error is expected here - i2s_channel_disable(this->tx_handle_); - this->tx_in_use_ = false; - this->update_rx_channel_(); -} - -bool I2SAudioComponent::update_rx_channel_() { - const bool needed = this->rx_in_use_ || this->tx_in_use_; - if (needed == this->rx_enabled_) - return true; - - esp_err_t err = needed ? i2s_channel_enable(this->rx_handle_) : i2s_channel_disable(this->rx_handle_); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to %s RX channel: %s", needed ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable"), - esp_err_to_name(err)); - return false; - } - this->rx_enabled_ = needed; - return true; -} - -} // namespace esphome::i2s_audio - -#endif // USE_ESP32 && USE_I2S_AUDIO_FULL_DUPLEX diff --git a/Integrations/ESPHome/components/i2s_audio/i2s_audio.h b/Integrations/ESPHome/components/i2s_audio/i2s_audio.h deleted file mode 100644 index ffbebc5..0000000 --- a/Integrations/ESPHome/components/i2s_audio/i2s_audio.h +++ /dev/null @@ -1,131 +0,0 @@ -#pragma once - -#ifdef USE_ESP32 - -#include "esphome/core/component.h" -#include "esphome/core/defines.h" -#include "esphome/core/helpers.h" -#include -#include - -namespace esphome::i2s_audio { - -class I2SAudioComponent; - -class I2SAudioBase : public Parented { - public: - 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; } - 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; } - void set_mclk_multiple(i2s_mclk_multiple_t mclk_multiple) { this->mclk_multiple_ = mclk_multiple; } - - protected: - 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_; - uint32_t sample_rate_; - bool use_apll_; - i2s_mclk_multiple_t mclk_multiple_; -}; - -class I2SAudioIn : public I2SAudioBase { -#ifdef USE_I2S_AUDIO_FULL_DUPLEX - public: - /// @brief Builds the RX configuration the parent uses to set up a full duplex channel pair. - /// @return false if this input cannot share a full duplex bus - virtual bool build_full_duplex_config(i2s_std_config_t &std_cfg) = 0; -#endif -}; - -class I2SAudioOut : public I2SAudioBase { -#ifdef USE_I2S_AUDIO_FULL_DUPLEX - public: - /// @brief Builds the TX configuration the parent uses to set up a full duplex channel pair. The channel - /// configuration (DMA layout, role, interrupt priority) is shared by both channels. - /// @return false if this output cannot share a full duplex bus - virtual bool build_full_duplex_config(i2s_chan_config_t &chan_cfg, i2s_std_config_t &std_cfg) { return false; } -#endif -}; - -class I2SAudioComponent final : public Component { - public: -#ifdef USE_I2S_AUDIO_FULL_DUPLEX - void setup() override; - - void set_audio_in(I2SAudioIn *audio_in) { this->audio_in_ = audio_in; } - void set_audio_out(I2SAudioOut *audio_out) { this->audio_out_ = audio_out; } - - /// @brief True when a microphone and a speaker share this bus at the same time. - bool is_full_duplex() const { return this->audio_in_ != nullptr && this->audio_out_ != nullptr; } - - /// @brief Enables the full duplex RX channel. Main loop only. - /// @return The enabled RX handle, or nullptr if the channel pair is unavailable - i2s_chan_handle_t acquire_rx_channel(); - /// @brief Releases the RX channel; it keeps running while the TX side needs its clocks. Main loop only. - void release_rx_channel(); - - /// @brief Starts the shared clocks and hands over the full duplex TX channel, still disabled, so the caller - /// can register callbacks and preload data before enabling it. Main loop only. - /// @return The TX handle, or nullptr if the channel pair is unavailable or another speaker holds it - i2s_chan_handle_t acquire_tx_channel(); - /// @brief Disables the TX channel and stops the shared clocks if the RX side is idle. Only the speaker that - /// acquired the channel may call this. Main loop only. - void release_tx_channel(); - /// @brief True while another speaker holds the full duplex TX channel - bool is_tx_in_use() const { return this->tx_in_use_; } -#endif - 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, - }}; - } - - void set_mclk_pin(int pin) { this->mclk_pin_ = pin; } - void set_bclk_pin(int pin) { this->bclk_pin_ = pin; } - void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; } - void set_port(int port) { this->port_ = port; } -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) - int get_port() const { return this->port_; } -#else - i2s_port_t get_port() const { return static_cast(this->port_); } -#endif - - void lock() { this->lock_.lock(); } - bool try_lock() { return this->lock_.try_lock(); } - void unlock() { this->lock_.unlock(); } - - protected: - Mutex lock_; - -#ifdef USE_I2S_AUDIO_FULL_DUPLEX - /// @brief Enables the RX channel while either side is active, since it drives the shared clocks. - bool update_rx_channel_(); - - I2SAudioIn *audio_in_{nullptr}; - I2SAudioOut *audio_out_{nullptr}; - i2s_chan_handle_t rx_handle_{nullptr}; - i2s_chan_handle_t tx_handle_{nullptr}; - bool rx_in_use_{false}; - bool tx_in_use_{false}; - bool rx_enabled_{false}; -#endif - int mclk_pin_{I2S_GPIO_UNUSED}; - int bclk_pin_{I2S_GPIO_UNUSED}; - int lrclk_pin_; - int port_{}; -}; - -} // namespace esphome::i2s_audio - -#endif // USE_ESP32 diff --git a/Integrations/ESPHome/components/i2s_audio/media_player/__init__.py b/Integrations/ESPHome/components/i2s_audio/media_player/__init__.py deleted file mode 100644 index b366d4f..0000000 --- a/Integrations/ESPHome/components/i2s_audio/media_player/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -import esphome.config_validation as cv - -CONFIG_SCHEMA = cv.invalid( - "The I2S audio media player has been removed. " - "Use the speaker media player component instead. " - "See https://esphome.io/components/media_player/speaker.html for details." -) diff --git a/Integrations/ESPHome/components/i2s_audio/microphone/__init__.py b/Integrations/ESPHome/components/i2s_audio/microphone/__init__.py deleted file mode 100644 index c217317..0000000 --- a/Integrations/ESPHome/components/i2s_audio/microphone/__init__.py +++ /dev/null @@ -1,159 +0,0 @@ -from esphome import pins -import esphome.codegen as cg -from esphome.components import audio, esp32, microphone -from esphome.components.adc import validate_adc_pin -import esphome.config_validation as cv -from esphome.const import ( - CONF_BITS_PER_SAMPLE, - CONF_CHANNEL, - CONF_ID, - CONF_NUM_CHANNELS, - CONF_SAMPLE_RATE, -) -from esphome.types import ConfigType - -from .. import ( - CONF_ADC_TYPE, - CONF_I2S_DIN_PIN, - CONF_LEFT, - CONF_MONO, - CONF_PDM, - CONF_PDM_DSR, - CONF_RIGHT, - I2SAudioIn, - i2s_audio_component_schema, - i2s_audio_ns, - register_i2s_audio_component, - validate_mclk_divisible_by_3, -) - -CODEOWNERS = ["@jesserockz"] -DEPENDENCIES = ["i2s_audio"] - -CONF_ADC_PIN = "adc_pin" -CONF_CORRECT_DC_OFFSET = "correct_dc_offset" - -I2SAudioMicrophone = i2s_audio_ns.class_( - "I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component -) - -INTERNAL_ADC_VARIANTS = [esp32.VARIANT_ESP32] -PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3, esp32.VARIANT_ESP32P4] - -i2s_pdm_dsr_t = cg.global_ns.enum("i2s_pdm_dsr_t") -I2S_PDM_DSR = { - 8: i2s_pdm_dsr_t.I2S_PDM_DSR_8S, - 16: i2s_pdm_dsr_t.I2S_PDM_DSR_16S, -} - - -def _validate_esp32_variant(config: ConfigType) -> ConfigType: - variant = esp32.get_esp32_variant() - if config[CONF_ADC_TYPE] == "external": - if config[CONF_PDM] and variant not in PDM_VARIANTS: - raise cv.Invalid(f"{variant} does not support PDM") - if ( - variant == esp32.VARIANT_ESP32 - and config.get(CONF_BITS_PER_SAMPLE) == 8 - and config.get(CONF_CHANNEL) in (CONF_LEFT, CONF_RIGHT) - ): - raise cv.Invalid("8-bit mono mode is not supported on ESP32") - return config - if config[CONF_ADC_TYPE] == "internal": - if variant not in INTERNAL_ADC_VARIANTS: - raise cv.Invalid(f"{variant} does not have an internal ADC") - return config - raise NotImplementedError - - -def _validate_channel(config: ConfigType) -> ConfigType: - if config[CONF_CHANNEL] == CONF_MONO: - raise cv.Invalid(f"I2S microphone does not support {CONF_MONO}.") - return config - - -def _set_num_channels_from_config(config: ConfigType) -> ConfigType: - if config[CONF_CHANNEL] in (CONF_LEFT, CONF_RIGHT): - config[CONF_NUM_CHANNELS] = 1 - else: - config[CONF_NUM_CHANNELS] = 2 - - return config - - -def _set_stream_limits(config: ConfigType) -> ConfigType: - audio.set_stream_limits( - min_bits_per_sample=config.get(CONF_BITS_PER_SAMPLE), - max_bits_per_sample=config.get(CONF_BITS_PER_SAMPLE), - min_channels=config.get(CONF_NUM_CHANNELS), - max_channels=config.get(CONF_NUM_CHANNELS), - min_sample_rate=config.get(CONF_SAMPLE_RATE), - max_sample_rate=config.get(CONF_SAMPLE_RATE), - )(config) - - return config - - -BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( - i2s_audio_component_schema( - I2SAudioMicrophone, - default_sample_rate=16000, - default_channel=CONF_RIGHT, - default_bits_per_sample="32bit", - ).extend( - { - cv.Optional(CONF_CORRECT_DC_OFFSET, default=False): cv.boolean, - } - ) -).extend(cv.COMPONENT_SCHEMA) - -CONFIG_SCHEMA = cv.All( - cv.typed_schema( - { - "internal": BASE_SCHEMA.extend( - { - cv.Required(CONF_ADC_PIN): validate_adc_pin, - } - ), - "external": BASE_SCHEMA.extend( - { - cv.Required(CONF_I2S_DIN_PIN): pins.internal_gpio_input_pin_number, - cv.Optional(CONF_PDM, default=False): cv.boolean, - cv.Optional(CONF_PDM_DSR, default=8): cv.enum( - I2S_PDM_DSR, int=True - ), - } - ), - }, - key=CONF_ADC_TYPE, - ), - _validate_esp32_variant, - _validate_channel, - _set_num_channels_from_config, - _set_stream_limits, - validate_mclk_divisible_by_3, -) - - -def _final_validate(config: ConfigType) -> None: - if config[CONF_ADC_TYPE] == "internal": - raise cv.Invalid( - "Internal ADC is no longer supported. Use an external I2S microphone instead." - ) - - -FINAL_VALIDATE_SCHEMA = _final_validate - - -async def to_code(config: ConfigType) -> None: - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await register_i2s_audio_component(var, config) - await microphone.register_microphone(var, config) - - cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) - cg.add(var.set_pdm(config[CONF_PDM])) - if esp32.get_esp32_variant() in PDM_VARIANTS: - cg.add(var.set_pdm_dsr(config[CONF_PDM_DSR])) - - cg.add(var.set_correct_dc_offset(config[CONF_CORRECT_DC_OFFSET])) diff --git a/Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.cpp deleted file mode 100644 index 98ef1ad..0000000 --- a/Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ /dev/null @@ -1,471 +0,0 @@ -#include "i2s_audio_microphone.h" - -#ifdef USE_ESP32 - -#include -#include - -#include "esphome/core/hal.h" -#include "esphome/core/log.h" - -#include "esphome/components/audio/audio.h" - -namespace esphome::i2s_audio { - -static const UBaseType_t MAX_LISTENERS = 16; - -static const uint32_t READ_DURATION_MS = 16; - -static const size_t TASK_STACK_SIZE = 4096; -static const ssize_t TASK_PRIORITY = 23; - -static const char *const TAG = "i2s_audio.microphone"; - -enum MicrophoneEventGroupBits : uint32_t { - COMMAND_STOP = (1 << 0), // stops the microphone task, set and cleared by ``loop`` - - TASK_STARTING = (1 << 10), // set by mic task, cleared by ``loop`` - TASK_RUNNING = (1 << 11), // set by mic task, cleared by ``loop`` - TASK_STOPPED = (1 << 13), // set by mic task, cleared by ``loop`` - - ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits -}; - -void I2SAudioMicrophone::setup() { - this->active_listeners_semaphore_ = xSemaphoreCreateCounting(MAX_LISTENERS, MAX_LISTENERS); - if (this->active_listeners_semaphore_ == nullptr) { - ESP_LOGE(TAG, "Creating semaphore failed"); - this->mark_failed(); - return; - } - - this->event_group_ = xEventGroupCreate(); - if (this->event_group_ == nullptr) { - ESP_LOGE(TAG, "Creating event group failed"); - this->mark_failed(); - return; - } - - this->configure_stream_settings_(); -} - -void I2SAudioMicrophone::dump_config() { - ESP_LOGCONFIG(TAG, - "Microphone:\n" - " Pin: %d\n" - " PDM: %s\n" - " DC offset correction: %s", - static_cast(this->din_pin_), YESNO(this->pdm_), YESNO(this->correct_dc_offset_)); -} - -void I2SAudioMicrophone::configure_stream_settings_() { - uint8_t channel_count = 1; - uint8_t bits_per_sample = 16; - if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { - bits_per_sample = this->slot_bit_width_; - } - - if (this->slot_mode_ == I2S_SLOT_MODE_STEREO) { - channel_count = 2; - } - -#ifdef USE_ESP32_VARIANT_ESP32 - // ESP32 reads audio aligned to a multiple of 2 bytes. For example, if configured for 24 bits per sample, then it will - // produce 32 bits per sample, where the actual data is in the most significant bits. Other ESP32 variants produce 24 - // bits per sample in this situation. - if (bits_per_sample < 16) { - bits_per_sample = 16; - } else if ((bits_per_sample > 16) && (bits_per_sample <= 32)) { - bits_per_sample = 32; - } -#endif - - if (this->pdm_) { - bits_per_sample = 16; // PDM mics are always 16 bits per sample - } - - this->audio_stream_info_ = audio::AudioStreamInfo(bits_per_sample, channel_count, this->sample_rate_); -} - -void I2SAudioMicrophone::start() { - if (this->is_failed()) - return; - - xSemaphoreTake(this->active_listeners_semaphore_, 0); -} - -i2s_clock_src_t I2SAudioMicrophone::get_clock_source_() const { -#ifdef I2S_CLK_SRC_APLL - if (this->use_apll_) { - return I2S_CLK_SRC_APLL; - } -#endif - return I2S_CLK_SRC_DEFAULT; -} - -void I2SAudioMicrophone::build_std_config_(i2s_std_config_t &std_cfg) const { - i2s_std_slot_config_t std_slot_cfg = - I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) this->slot_bit_width_, this->slot_mode_); - std_slot_cfg.slot_bit_width = this->slot_bit_width_; - std_slot_cfg.slot_mask = this->std_slot_mask_; - - i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config(); - pin_config.din = this->din_pin_; - - std_cfg = { - .clk_cfg = - { - .sample_rate_hz = this->sample_rate_, - .clk_src = this->get_clock_source_(), - .mclk_multiple = this->mclk_multiple_, - }, - .slot_cfg = std_slot_cfg, - .gpio_cfg = pin_config, - }; -} - -#ifdef USE_I2S_AUDIO_FULL_DUPLEX -bool I2SAudioMicrophone::build_full_duplex_config(i2s_std_config_t &std_cfg) { - if (this->pdm_) { - return false; - } - this->build_std_config_(std_cfg); - return true; -} -#endif - -bool I2SAudioMicrophone::start_driver_() { -#ifdef USE_I2S_AUDIO_FULL_DUPLEX - if (this->parent_->is_full_duplex()) { - this->rx_handle_ = this->parent_->acquire_rx_channel(); - if (this->rx_handle_ == nullptr) { - ESP_LOGE(TAG, "Full duplex channel unavailable"); - return false; - } - this->configure_stream_settings_(); - return true; - } -#endif - - if (!this->parent_->try_lock()) { - return false; // Waiting for another i2s to return lock - } - this->locked_driver_ = true; - esp_err_t err; - - 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_LOGE(TAG, "Error creating channel: %s", esp_err_to_name(err)); - return false; - } - -#if SOC_I2S_SUPPORTS_PDM_RX - if (this->pdm_) { - i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config(); - i2s_pdm_rx_clk_config_t clk_cfg = { - .sample_rate_hz = this->sample_rate_, - .clk_src = this->get_clock_source_(), - .mclk_multiple = this->mclk_multiple_, - .dn_sample_mode = this->pdm_dsr_, - }; - - 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_config_t std_cfg; - this->build_std_config_(std_cfg); - /* Initialize the channel */ - err = i2s_channel_init_std_mode(this->rx_handle_, &std_cfg); - } - if (err != ESP_OK) { - ESP_LOGE(TAG, "Error initializing channel: %s", esp_err_to_name(err)); - return false; - } - - /* Before reading data, start the RX channel first */ - err = i2s_channel_enable(this->rx_handle_); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Enabling failed: %s", esp_err_to_name(err)); - return false; - } - - this->configure_stream_settings_(); // redetermine the settings in case some settings were changed after compilation - - return true; -} - -void I2SAudioMicrophone::stop() { - if (this->state_ == microphone::STATE_STOPPED || this->is_failed()) - return; - - xSemaphoreGive(this->active_listeners_semaphore_); -} - -void I2SAudioMicrophone::stop_driver_() { - // There is no harm continuing to unload the driver if an error is ever returned by the various functions. This - // ensures that we stop/unload the driver when it only partially starts. - -#ifdef USE_I2S_AUDIO_FULL_DUPLEX - if (this->parent_->is_full_duplex()) { - // The parent owns the channel pair, so only hand the RX channel back - if (this->rx_handle_ != nullptr) { - this->parent_->release_rx_channel(); - this->rx_handle_ = nullptr; - } - return; - } -#endif - - esp_err_t err; - if (this->rx_handle_ != nullptr) { - /* Have to stop the channel before deleting it */ - err = i2s_channel_disable(this->rx_handle_); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Error stopping: %s", esp_err_to_name(err)); - } - /* 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 channel: %s", esp_err_to_name(err)); - } - this->rx_handle_ = nullptr; - } - if (this->locked_driver_) { - this->parent_->unlock(); - this->locked_driver_ = false; - } -} - -void I2SAudioMicrophone::mic_task(void *params) { - I2SAudioMicrophone *this_microphone = (I2SAudioMicrophone *) params; - xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STARTING); - - { // Ensures the samples vector is freed when the task stops - - const size_t bytes_to_read = this_microphone->audio_stream_info_.ms_to_bytes(READ_DURATION_MS); - std::vector samples; - samples.reserve(bytes_to_read); - - xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_RUNNING); - - while (!(xEventGroupGetBits(this_microphone->event_group_) & MicrophoneEventGroupBits::COMMAND_STOP)) { - if (this_microphone->data_callbacks_.size() > 0) { - samples.resize(bytes_to_read); - size_t bytes_read = this_microphone->read_(samples.data(), bytes_to_read, 2 * READ_DURATION_MS); - samples.resize(bytes_read); - if (this_microphone->correct_dc_offset_) { - this_microphone->fix_dc_offset_(samples); - } - this_microphone->data_callbacks_.call(samples); - } else { - vTaskDelay(pdMS_TO_TICKS(READ_DURATION_MS)); - } - } - } - - xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STOPPED); - while (true) { - // Continuously delay until the loop method deletes the task - vTaskDelay(pdMS_TO_TICKS(10)); - } -} - -void I2SAudioMicrophone::fix_dc_offset_(std::vector &data) { - /** - * From https://www.musicdsp.org/en/latest/Filters/135-dc-filter.html: - * - * y(n) = x(n) - x(n-1) + R * y(n-1) - * R = 1 - (pi * 2 * frequency / samplerate) - * - * From https://en.wikipedia.org/wiki/Hearing_range: - * The human range is commonly given as 20Hz up. - * - * From https://en.wikipedia.org/wiki/High-resolution_audio: - * A reasonable upper bound for sample rate seems to be 96kHz. - * - * Calculate R value for 20Hz on a 96kHz sample rate: - * R = 1 - (pi * 2 * 20 / 96000) - * R = 0.9986910031 - * - * Transform floating point to bit-shifting approximation: - * output = input - prev_input + R * prev_output - * output = input - prev_input + (prev_output - (prev_output >> S)) - * - * Approximate bit-shift value S from R: - * R = 1 - (1 >> S) - * R = 1 - (1 / 2^S) - * R = 1 - 2^-S - * 0.9986910031 = 1 - 2^-S - * S = 9.57732 ~= 10 - * - * Actual R from S: - * R = 1 - 2^-10 = 0.9990234375 - * - * Confirm this has effect outside human hearing on 96000kHz sample: - * 0.9990234375 = 1 - (pi * 2 * f / 96000) - * f = 14.9208Hz - * - * Confirm this has effect outside human hearing on PDM 16kHz sample: - * 0.9990234375 = 1 - (pi * 2 * f / 16000) - * f = 2.4868Hz - * - */ - const uint8_t dc_filter_shift = 10; - const size_t bytes_per_sample = this->audio_stream_info_.samples_to_bytes(1); - const uint32_t total_samples = this->audio_stream_info_.bytes_to_samples(data.size()); - for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) { - const uint32_t byte_index = sample_index * bytes_per_sample; - int32_t input = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample); - int32_t output = input - this->dc_offset_prev_input_ + - (this->dc_offset_prev_output_ - (this->dc_offset_prev_output_ >> dc_filter_shift)); - this->dc_offset_prev_input_ = input; - this->dc_offset_prev_output_ = output; - audio::pack_q31_as_audio_sample(output, &data[byte_index], bytes_per_sample); - } -} - -size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, uint32_t timeout_ms) { - size_t bytes_read = 0; - esp_err_t err = i2s_channel_read(this->rx_handle_, buf, len, &bytes_read, timeout_ms); - if ((err != ESP_OK) && ((err != ESP_ERR_TIMEOUT) || (timeout_ms != 0))) { - // Ignore ESP_ERR_TIMEOUT if timeout_ms = 0, as it will read the data on the next call - if (!this->status_has_warning()) { - // Avoid spamming the logs with the error message if its repeated - ESP_LOGW(TAG, "Read error: %s", esp_err_to_name(err)); - } - this->status_set_warning(); - return 0; - } - if ((bytes_read == 0) && (timeout_ms > 0)) { - this->status_set_warning(); - return 0; - } - this->status_clear_warning(); -#ifdef USE_ESP32_VARIANT_ESP32 - // For ESP32 16-bit standard mono mode, adjacent samples need to be swapped. - if (this->slot_mode_ == I2S_SLOT_MODE_MONO && this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_16BIT && !this->pdm_) { - int16_t *samples = reinterpret_cast(buf); - size_t sample_count = bytes_read / sizeof(int16_t); - for (size_t i = 0; i + 1 < sample_count; i += 2) { - int16_t tmp = samples[i]; - samples[i] = samples[i + 1]; - samples[i + 1] = tmp; - } - } -#endif - return bytes_read; -} - -void I2SAudioMicrophone::loop() { - uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); - - if (event_group_bits & MicrophoneEventGroupBits::TASK_STARTING) { - ESP_LOGV(TAG, "Task started, attempting to allocate buffer"); - xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_STARTING); - } - - if (event_group_bits & MicrophoneEventGroupBits::TASK_RUNNING) { - ESP_LOGV(TAG, "Task is running and reading data"); - - xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_RUNNING); - this->state_ = microphone::STATE_RUNNING; - } - - if ((event_group_bits & MicrophoneEventGroupBits::TASK_STOPPED)) { - ESP_LOGV(TAG, "Task finished, freeing resources and uninstalling driver"); - - vTaskDelete(this->task_handle_); - this->task_handle_ = nullptr; - this->stop_driver_(); - xEventGroupClearBits(this->event_group_, ALL_BITS); - this->status_clear_error(); - - this->state_ = microphone::STATE_STOPPED; - } - - // Start the microphone if any semaphores are taken - if ((uxSemaphoreGetCount(this->active_listeners_semaphore_) < MAX_LISTENERS) && - (this->state_ == microphone::STATE_STOPPED)) { - this->state_ = microphone::STATE_STARTING; - } - - // Stop the microphone if all semaphores are returned - if ((uxSemaphoreGetCount(this->active_listeners_semaphore_) == MAX_LISTENERS) && - (this->state_ == microphone::STATE_RUNNING)) { - this->state_ = microphone::STATE_STOPPING; - } - - switch (this->state_) { - case microphone::STATE_STARTING: - if (this->status_has_error()) { - break; - } - - if (!this->start_driver_()) { - ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); - this->status_momentary_error("driver_fail", 1000); - this->stop_driver_(); // Stop/frees whatever possibly started - break; - } - - if (this->task_handle_ == nullptr) { - xTaskCreate(I2SAudioMicrophone::mic_task, "mic_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, - &this->task_handle_); - - if (this->task_handle_ == nullptr) { - ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); - this->status_momentary_error("task_fail", 1000); - this->stop_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt - } - } - - break; - case microphone::STATE_RUNNING: - break; - case microphone::STATE_STOPPING: - xEventGroupSetBits(this->event_group_, MicrophoneEventGroupBits::COMMAND_STOP); - break; - case microphone::STATE_STOPPED: - break; - } -} - -} // namespace esphome::i2s_audio - -#endif // USE_ESP32 diff --git a/Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.h b/Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.h deleted file mode 100644 index 972c986..0000000 --- a/Integrations/ESPHome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once - -#ifdef USE_ESP32 - -#include "../i2s_audio.h" - -#include "esphome/components/microphone/microphone.h" -#include "esphome/core/component.h" - -#include -#include -#include -#include - -namespace esphome::i2s_audio { - -class I2SAudioMicrophone final : public I2SAudioIn, public microphone::Microphone, public Component { - public: - void setup() override; - void dump_config() override; - void start() override; - void stop() override; - - void loop() override; - - void set_correct_dc_offset(bool correct_dc_offset) { this->correct_dc_offset_ = correct_dc_offset; } - - void set_din_pin(int8_t pin) { this->din_pin_ = (gpio_num_t) pin; } - - void set_pdm(bool pdm) { this->pdm_ = pdm; } - -#if SOC_I2S_SUPPORTS_PDM_RX - void set_pdm_dsr(i2s_pdm_dsr_t pdm_dsr) { this->pdm_dsr_ = pdm_dsr; } -#endif - -#ifdef USE_I2S_AUDIO_FULL_DUPLEX - bool build_full_duplex_config(i2s_std_config_t &std_cfg) override; -#endif - - protected: - /// @brief Builds the standard (non-PDM) mode configuration from the configured settings. - void build_std_config_(i2s_std_config_t &std_cfg) const; - - i2s_clock_src_t get_clock_source_() const; - - /// @brief Starts the I2S driver. Updates the ``audio_stream_info_`` member variable with the current setttings. - /// @return True if succesful, false otherwise - bool start_driver_(); - - /// @brief Stops the I2S driver. - void stop_driver_(); - - /// @brief Attempts to correct a microphone DC offset; e.g., a microphones silent level is offset from 0. Applies a - /// correction offset that is updated using an exponential moving average for all samples away from 0. - /// @param data - void fix_dc_offset_(std::vector &data); - - size_t read_(uint8_t *buf, size_t len, uint32_t timeout_ms); - - /// @brief Sets the Microphone ``audio_stream_info_`` member variable to the configured I2S settings. - void configure_stream_settings_(); - - static void mic_task(void *params); - - SemaphoreHandle_t active_listeners_semaphore_{nullptr}; - EventGroupHandle_t event_group_{nullptr}; - - TaskHandle_t task_handle_{nullptr}; - - gpio_num_t din_pin_{I2S_GPIO_UNUSED}; - i2s_chan_handle_t rx_handle_{nullptr}; - bool pdm_{false}; -#if SOC_I2S_SUPPORTS_PDM_RX - i2s_pdm_dsr_t pdm_dsr_{I2S_PDM_DSR_8S}; -#endif - - bool correct_dc_offset_; - bool locked_driver_{false}; - int32_t dc_offset_prev_input_{0}; - int32_t dc_offset_prev_output_{0}; -}; - -} // namespace esphome::i2s_audio - -#endif // USE_ESP32 diff --git a/Integrations/ESPHome/components/i2s_audio/speaker/__init__.py b/Integrations/ESPHome/components/i2s_audio/speaker/__init__.py deleted file mode 100644 index 0465ab5..0000000 --- a/Integrations/ESPHome/components/i2s_audio/speaker/__init__.py +++ /dev/null @@ -1,274 +0,0 @@ -from esphome import pins -import esphome.codegen as cg -from esphome.components import audio, esp32, speaker -from esphome.config_helpers import filter_source_files_from_defines -import esphome.config_validation as cv -from esphome.const import ( - CONF_BITS_PER_SAMPLE, - CONF_BUFFER_DURATION, - CONF_CHANNEL, - CONF_ID, - CONF_MODE, - CONF_NEVER, - CONF_NUM_CHANNELS, - CONF_SAMPLE_RATE, - CONF_TIMEOUT, -) -from esphome.types import ConfigType - -from .. import ( - CONF_I2S_COMM_FMT, - CONF_I2S_DOUT_PIN, - CONF_I2S_MODE, - CONF_LEFT, - CONF_MCLK_MULTIPLE, - CONF_MONO, - CONF_PRIMARY, - CONF_RIGHT, - CONF_SPDIF_MODE, - CONF_STEREO, - CONF_USE_APLL, - I2SAudioOut, - i2s_audio_component_schema, - i2s_audio_ns, - register_i2s_audio_component, - validate_mclk_divisible_by_3, -) - -AUTO_LOAD = ["audio"] -CODEOWNERS = ["@jesserockz", "@kahrendt"] -DEPENDENCIES = ["i2s_audio"] - -I2SAudioSpeakerBase = i2s_audio_ns.class_( - "I2SAudioSpeakerBase", cg.Component, speaker.Speaker, I2SAudioOut -) -I2SAudioSpeaker = i2s_audio_ns.class_("I2SAudioSpeaker", I2SAudioSpeakerBase) - -CONF_DAC_TYPE = "dac_type" - -I2SAudioSpeakerBase = i2s_audio_ns.class_( - "I2SAudioSpeakerBase", cg.Component, speaker.Speaker, I2SAudioOut -) -I2SAudioSpeaker = i2s_audio_ns.class_("I2SAudioSpeaker", I2SAudioSpeakerBase) -I2SAudioSpeakerSPDIF = i2s_audio_ns.class_("I2SAudioSpeakerSPDIF", I2SAudioSpeakerBase) - -I2SCommFmt = i2s_audio_ns.enum("I2SCommFmt", is_class=True) - -I2SCommFmt = i2s_audio_ns.enum("I2SCommFmt", is_class=True) - -i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") -INTERNAL_DAC_OPTIONS = { - CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, - CONF_RIGHT: i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN, - CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, -} - -i2s_comm_format_t = cg.global_ns.enum("i2s_comm_format_t") -I2C_COMM_FMT_OPTIONS = { - "stand_i2s": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_I2S, - "stand_msb": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_MSB, - "stand_pcm_short": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_PCM_SHORT, - "stand_pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_PCM_LONG, - "stand_max": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_MAX, - "i2s_msb": i2s_comm_format_t.I2S_COMM_FORMAT_I2S_MSB, - "i2s_lsb": i2s_comm_format_t.I2S_COMM_FORMAT_I2S_LSB, - "pcm": i2s_comm_format_t.I2S_COMM_FORMAT_PCM, - "pcm_short": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_SHORT, - "pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_LONG, -} - -INTERNAL_DAC_VARIANTS = [esp32.VARIANT_ESP32] - - -def _set_num_channels_from_config(config: ConfigType) -> ConfigType: - if config[CONF_CHANNEL] in (CONF_MONO, CONF_LEFT, CONF_RIGHT): - config[CONF_NUM_CHANNELS] = 1 - else: - config[CONF_NUM_CHANNELS] = 2 - - return config - - -def _set_stream_limits(config: ConfigType) -> ConfigType: - if config.get(CONF_SPDIF_MODE, False): - # SPDIF mode: 16/24/32-bit audio and stereo at configured sample rate - audio.set_stream_limits( - min_bits_per_sample=16, - max_bits_per_sample=32, - min_channels=2, - max_channels=2, - min_sample_rate=config.get(CONF_SAMPLE_RATE), - max_sample_rate=config.get(CONF_SAMPLE_RATE), - )(config) - return config - - # The original ESP32 cannot lay out sub-16-bit slots that match ESPHome's packed audio, so the smallest - # stream it accepts is 16-bit (see start_i2s_driver); the other variants handle 8-bit. - min_bits_per_sample = 16 if esp32.get_esp32_variant() == esp32.VARIANT_ESP32 else 8 - - # The configured bits per sample sets the I2S slot width, but the speaker narrows wider streams down to it - # in place before clocking them out (see start_i2s_driver). Advertise up to 32-bit so those wider streams - # are accepted rather than forcing an upstream conversion. - max_bits_per_sample = 32 - - if config[CONF_I2S_MODE] == CONF_PRIMARY: - # Primary mode can reconfigure the bus to the incoming sample rate and channel count. - audio.set_stream_limits( - min_bits_per_sample=min_bits_per_sample, - max_bits_per_sample=max_bits_per_sample, - min_channels=1, - max_channels=2, - min_sample_rate=16000, - max_sample_rate=48000, - )(config) - else: - # Secondary mode has unmodifiable min/max sample rates - audio.set_stream_limits( - min_bits_per_sample=min_bits_per_sample, - max_bits_per_sample=max_bits_per_sample, - min_channels=1, - max_channels=2, - min_sample_rate=config.get(CONF_SAMPLE_RATE), - max_sample_rate=config.get(CONF_SAMPLE_RATE), - )(config) - - return config - - -def _select_speaker_class(config: ConfigType) -> ConfigType: - """Override ID type when SPDIF mode is enabled.""" - if config.get(CONF_SPDIF_MODE, False): - config[CONF_ID].type = I2SAudioSpeakerSPDIF - return config - - -def _validate_esp32_variant(config: ConfigType) -> ConfigType: - variant = esp32.get_esp32_variant() - if config[CONF_DAC_TYPE] == "internal": - if variant not in INTERNAL_DAC_VARIANTS: - raise cv.Invalid(f"{variant} does not have an internal DAC") - elif variant == esp32.VARIANT_ESP32 and config[CONF_BITS_PER_SAMPLE] == 8: - # The original ESP32 I2S peripheral packs each sample into a whole number of 16-bit words, so an - # 8-bit slot does not line up with ESPHome's tightly packed audio (see start_i2s_driver). Reject it - # at config time rather than emitting corrupted output at runtime. - raise cv.Invalid("8-bit audio is not supported on the original ESP32") - return config - - -BASE_SCHEMA = ( - speaker.SPEAKER_SCHEMA.extend( - i2s_audio_component_schema( - I2SAudioSpeaker, - default_sample_rate=16000, - default_channel=CONF_MONO, - default_bits_per_sample="16bit", - ) - ) - .extend( - { - cv.Optional( - CONF_BUFFER_DURATION, default="500ms" - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_TIMEOUT, default="500ms"): cv.Any( - cv.positive_time_period_milliseconds, - cv.one_of(CONF_NEVER, lower=True), - ), - } - ) - .extend(cv.COMPONENT_SCHEMA) -) - - -CONFIG_SCHEMA = cv.All( - cv.typed_schema( - { - "internal": BASE_SCHEMA.extend( - { - cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True), - } - ), - "external": BASE_SCHEMA.extend( - { - cv.Required( - CONF_I2S_DOUT_PIN - ): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_I2S_COMM_FMT, default="stand_i2s"): cv.one_of( - *I2C_COMM_FMT_OPTIONS, lower=True - ), - cv.Optional(CONF_SPDIF_MODE, default=False): cv.boolean, - } - ), - }, - key=CONF_DAC_TYPE, - ), - _validate_esp32_variant, - _set_num_channels_from_config, - _set_stream_limits, - _select_speaker_class, - validate_mclk_divisible_by_3, -) - - -def _final_validate(config: ConfigType) -> None: - if config[CONF_DAC_TYPE] == "internal": - raise cv.Invalid( - "Internal DAC is no longer supported. Use an external I2S DAC instead." - ) - if config[CONF_I2S_COMM_FMT] == "stand_max": - raise cv.Invalid("I2S standard max format is no longer supported.") - - if config.get(CONF_SPDIF_MODE, False): - # SPDIF mode specific validations - if config[CONF_SAMPLE_RATE] not in [44100, 48000]: - raise cv.Invalid( - "SPDIF mode only supports 44100 Hz or 48000 Hz sample rates" - ) - if config[CONF_CHANNEL] != CONF_STEREO: - raise cv.Invalid("SPDIF mode only supports stereo channel configuration") - if not config[CONF_USE_APLL]: - raise cv.Invalid( - "SPDIF mode requires 'use_apll: true' for accurate clock generation" - ) - if config[CONF_I2S_MODE] != CONF_PRIMARY: - raise cv.Invalid("SPDIF mode requires 'i2s_mode: primary'") - if config[CONF_I2S_COMM_FMT] != "stand_i2s": - raise cv.Invalid("SPDIF mode requires 'i2s_comm_fmt: stand_i2s'") - if config[CONF_MCLK_MULTIPLE] != 256: - raise cv.Invalid("SPDIF mode requires 'mclk_multiple: 256'") - - -FINAL_VALIDATE_SCHEMA = _final_validate - - -async def to_code(config: ConfigType) -> None: - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await register_i2s_audio_component(var, config) - await speaker.register_speaker(var, config) - - cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) - - is_spdif = config.get(CONF_SPDIF_MODE, False) - if is_spdif: - cg.add_define("USE_I2S_AUDIO_SPDIF_MODE") - else: - fmt = I2SCommFmt.STANDARD # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long - if config[CONF_I2S_COMM_FMT] in ["stand_msb", "i2s_lsb"]: - fmt = I2SCommFmt.MSB - elif config[CONF_I2S_COMM_FMT] in ["stand_pcm_short", "pcm_short", "pcm"]: - fmt = I2SCommFmt.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])) - - -# The SPDIF encoder and speaker are fully #ifdef'd on USE_I2S_AUDIO_SPDIF_MODE, -# set only when spdif_mode is enabled. -FILTER_SOURCE_FILES = filter_source_files_from_defines( - { - "spdif_encoder.cpp": "USE_I2S_AUDIO_SPDIF_MODE", - "i2s_audio_spdif.cpp": "USE_I2S_AUDIO_SPDIF_MODE", - } -) diff --git a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_spdif.cpp b/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_spdif.cpp deleted file mode 100644 index ed5145d..0000000 --- a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_spdif.cpp +++ /dev/null @@ -1,504 +0,0 @@ -#include "i2s_audio_spdif.h" - -#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_SPDIF_MODE) - -#include - -#include "esphome/components/audio/audio.h" -#include "esphome/components/audio/audio_transfer_buffer.h" - -#include "esphome/core/hal.h" -#include "esphome/core/log.h" - -#include "esp_timer.h" - -namespace esphome::i2s_audio { - -static const char *const TAG = "i2s_audio.spdif"; - -// SPDIF mode adds overhead as each sample is encapsulated in a subframe; -// each DMA buffer can hold only 192 samples (~4ms each vs. ~15ms for standard I2S). -// To match the standard I2S buffering duration, we use more buffers to minimize -// the impact of the overhead, such as stuttering or audio/silence oscillation. -// 15 buffers x 4ms = 60ms of DMA buffering (same as 4 x 15ms for standard) -static constexpr size_t SPDIF_DMA_BUFFERS_COUNT = 15; - -// Number of DMA events between upstream callbacks (~16ms = 4 events x 4ms each). -// Matches non-SPDIF timing to prevent overwhelming upstream sync algorithms. -static constexpr uint32_t SPDIF_DMA_EVENTS_PER_CALLBACK = 4; - -// Brief retry wait used by play() to catch short free-space windows during rapid track transitions. -static constexpr uint32_t SPDIF_PLAY_RETRY_WAIT_MS = 5; - -static constexpr size_t SPDIF_I2S_EVENT_QUEUE_COUNT = 2 * SPDIF_DMA_BUFFERS_COUNT; - -// Static callback functions for SPDIF encoder (avoids std::function overhead) -static esp_err_t spdif_preload_cb(void *user_ctx, uint32_t *data, size_t size, TickType_t ticks_to_wait) { - auto *speaker = static_cast(user_ctx); - size_t bytes_written = 0; - esp_err_t err = i2s_channel_preload_data(speaker->get_tx_handle(), data, size, &bytes_written); - if (err != ESP_OK || bytes_written != size) { - ESP_LOGV(TAG, "Preload failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size); - return (err != ESP_OK) ? err : ESP_ERR_NO_MEM; - } - return ESP_OK; -} - -static esp_err_t spdif_write_cb(void *user_ctx, uint32_t *data, size_t size, TickType_t ticks_to_wait) { - auto *speaker = static_cast(user_ctx); - size_t bytes_written = 0; - esp_err_t err = i2s_channel_write(speaker->get_tx_handle(), data, size, &bytes_written, ticks_to_wait); - if (err != ESP_OK) { - ESP_LOGV(TAG, "I2S write failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size); - } - return err; -} - -void I2SAudioSpeakerSPDIF::setup() { - I2SAudioSpeakerBase::setup(); - if (this->is_failed()) { - return; - } - - this->spdif_encoder_ = new SPDIFEncoder(); - if (!this->spdif_encoder_->setup()) { - ESP_LOGE(TAG, "Encoder setup failed"); - this->mark_failed(); - return; - } - - // Configure channel status block with the sample rate - this->spdif_encoder_->set_sample_rate(this->sample_rate_); - - // Separate callbacks for preload (during underflow recovery) and normal writes - this->spdif_encoder_->set_preload_callback(spdif_preload_cb, this); - this->spdif_encoder_->set_write_callback(spdif_write_cb, this); -} - -void I2SAudioSpeakerSPDIF::dump_config() { - I2SAudioSpeakerBase::dump_config(); - ESP_LOGCONFIG(TAG, - " SPDIF Mode: YES\n" - " Sample Rate: %" PRIu32 " Hz", - this->sample_rate_); -} - -void I2SAudioSpeakerSPDIF::on_task_stopped() { this->spdif_silence_start_ = 0; } - -size_t I2SAudioSpeakerSPDIF::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) { - if (this->is_failed()) { - ESP_LOGE(TAG, "Setup failed; cannot play audio"); - return 0; - } - - // In SPDIF mode, keep accepting upstream audio while the speaker task is active. - // This avoids transient drops during stop/start transitions. - const bool task_active = (this->speaker_task_handle_ != nullptr); - - if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) { - this->start(); - } - - if (!task_active && this->state_ != speaker::STATE_RUNNING) { - // Unable to write data to a running speaker, so delay the max amount of time so it can get ready - vTaskDelay(ticks_to_wait); - ticks_to_wait = 0; - } - - size_t bytes_written = 0; - if (this->state_ == speaker::STATE_RUNNING || task_active) { - std::shared_ptr temp_ring_buffer = this->audio_ring_buffer_.lock(); - if (temp_ring_buffer != nullptr) { - // In SPDIF mode, a tiny wait helps avoid transient 0-byte writes during short backpressure windows. - TickType_t effective_ticks_to_wait = ticks_to_wait; - if (effective_ticks_to_wait == 0) { - effective_ticks_to_wait = pdMS_TO_TICKS(1); - } - bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, effective_ticks_to_wait); - if (bytes_written == 0 && length > 0) { - // Retry once to catch short free-space windows during rapid seek/track transitions. - bytes_written = - temp_ring_buffer->write_without_replacement((void *) data, length, pdMS_TO_TICKS(SPDIF_PLAY_RETRY_WAIT_MS)); - } - } - } - - return bytes_written; -} - -void I2SAudioSpeakerSPDIF::run_speaker_task() { - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING); - - // Reset SPDIF encoder at task start to ensure clean state - // (previous task may have left stale data in encoder buffer) - if (this->spdif_encoder_ != nullptr) { - this->spdif_encoder_->reset(); - } - - // Reset lockstep records queue so it starts paired with the (also-reset) i2s_event_queue_. - xQueueReset(this->write_records_queue_); - - // The DMA buffers may have more bits per sample, so calculate buffer sizes based on the input audio stream info - const size_t bytes_per_frame = this->current_stream_info_.frames_to_bytes(1); - - // For SPDIF mode, one DMA buffer = one SPDIF block = 192 PCM frames (~4 ms at 48 kHz), - // not the ~15 ms a standard I2S DMA buffer holds. Derive the DMA floor from actual block size. - const uint32_t frames_to_fill_single_dma_buffer = SPDIF_BLOCK_SAMPLES; - const size_t bytes_to_fill_single_dma_buffer = - this->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer); - const size_t dma_buffers_floor_bytes = bytes_to_fill_single_dma_buffer * SPDIF_DMA_BUFFERS_COUNT; - - // Round the ring buffer size down to a multiple of bytes_per_frame so the wrap boundary stays frame-aligned and - // avoids unnecessary single-frame splices. Ensure it is at least large enough to cover all DMA buffers. - const size_t requested_ring_buffer_bytes = - (this->current_stream_info_.ms_to_bytes(this->buffer_duration_ms_) / bytes_per_frame) * bytes_per_frame; - const size_t ring_buffer_size = std::max(dma_buffers_floor_bytes, requested_ring_buffer_bytes); - - bool successful_setup = false; - std::unique_ptr audio_source; - - { - std::shared_ptr temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); - audio_source = audio::RingBufferAudioSource::create(temp_ring_buffer, bytes_to_fill_single_dma_buffer, - static_cast(bytes_per_frame)); - if (audio_source != nullptr) { - this->audio_ring_buffer_ = temp_ring_buffer; - successful_setup = true; - } - } - - if (!successful_setup) { - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); - } else { - // Preload DMA buffers with SPDIF-encoded silence before enabling the channel. - // This ensures the first data transmitted is valid SPDIF (not raw zeros from - // auto_clear) and prevents phantom DMA events before real audio is available. - // Each preloaded block pushes a 0-real-frame record so that the corresponding - // on_sent events drain in lockstep without crediting any audio frames. - this->spdif_encoder_->set_preload_mode(true); - for (size_t i = 0; i < SPDIF_DMA_BUFFERS_COUNT; i++) { - // i2s_channel_preload_data is non-blocking (returns immediately when the preload buffer fills), so no wait. - esp_err_t preload_err = this->spdif_encoder_->flush_with_silence(0); - if (preload_err != ESP_OK) { - break; // DMA preload buffer full or error - } - const uint32_t silence_record = 0; - xQueueSendToBack(this->write_records_queue_, &silence_record, 0); - } - this->spdif_encoder_->set_preload_mode(false); - this->spdif_encoder_->reset(); // Clean encoder state for the main loop - - // Now register the callback and enable the channel - xQueueReset(this->i2s_event_queue_); - const i2s_event_callbacks_t callbacks = {.on_sent = i2s_on_sent_cb}; - i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this); - i2s_channel_enable(this->tx_handle_); - - // Always-fill model: each iteration produces exactly one SPDIF block (= one DMA buffer). - // We drain real PCM up to one block from the ring buffer and silence-pad any remainder. - // Blocking writes pace the loop at the DMA consumption rate. This mirrors the standard - // I2S speaker pattern (PR #16317): fill what you can, then silence-pad whatever is still - // missing to complete the DMA buffer. - const uint32_t block_duration_us = this->current_stream_info_.frames_to_microseconds(SPDIF_BLOCK_SAMPLES); - // Sized to absorb the worst case where every DMA buffer is full when we issue the write. - const TickType_t write_timeout_ticks = - pdMS_TO_TICKS(((block_duration_us * (SPDIF_DMA_BUFFERS_COUNT + 1)) + 999) / 1000); - // Brief read budget when the ring buffer is empty (~half a block). - const TickType_t read_timeout_ticks = pdMS_TO_TICKS(((block_duration_us / 2) + 999) / 1000); - - // SPDIF Callback Decimation: fire every 4th DMA event (~16ms), matching non-SPDIF timing. - uint32_t spdif_pending_frames = 0; - int64_t spdif_pending_timestamp = 0; - uint32_t spdif_dma_event_count = 0; - - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING); - - // SPDIF continuous mode: loop runs indefinitely, outputting silence when no audio data - // to keep the receiver synced. Exits only via break (stream info change, silence timeout, - // lockstep desync, dropped event, or partial-write failure). - while (true) { - uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); - - if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP); - // The ISR pairs COMMAND_STOP with ERR_DROPPED_EVENT when it has to discard a completion - // event; that desyncs the lockstep queues permanently and the only safe recovery is a full - // task restart. - if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) { - ESP_LOGV(TAG, "Exiting: ISR dropped event, restarting to recover lockstep"); - break; - } - // User-initiated stop. In SPDIF continuous mode, transition to silence output rather - // than tearing the task down. - this->spdif_silence_start_ = millis(); - ESP_LOGV(TAG, "COMMAND_STOP received, continuing in silence mode"); - } - if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY) { - // SPDIF continuous mode never tears the channel down on graceful stop. Clear the flag and - // let the audio simply drain through the always-fill loop into the silence-timeout path. - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY); - } - - if (this->audio_stream_info_ != this->current_stream_info_) { - ESP_LOGV(TAG, "Exiting: stream info changed"); - break; - } - - // Drain ISR completion events, popping a matching record for each. - int64_t write_timestamp; - bool lockstep_broken = false; - while (xQueueReceive(this->i2s_event_queue_, &write_timestamp, 0)) { - // Lockstep: pop the matching record (real audio frames packed into this DMA block). - // Records are pushed by the task right after each successful block commit, so the FIFO - // order matches DMA completion order. Empty records queue here means lockstep broke. - uint32_t real_frames = 0; - if (xQueueReceive(this->write_records_queue_, &real_frames, 0) != pdTRUE) { - ESP_LOGV(TAG, "Event without matching write record"); - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC); - lockstep_broken = true; - break; - } - - // Per-block timestamp adjustment: shift back by the silence-padding portion of the block - // so the reported timestamp reflects when the last real sample left the wire. - uint32_t frames_sent = real_frames; - if (real_frames < SPDIF_BLOCK_SAMPLES) { - const uint32_t frames_zeroed = SPDIF_BLOCK_SAMPLES - real_frames; - write_timestamp -= this->current_stream_info_.frames_to_microseconds(frames_zeroed); - } - - spdif_dma_event_count++; - // Accumulate frames; keep the latest timestamp so the callback reports when the last - // sample left the wire, not the first. - if (frames_sent > 0) { - spdif_pending_timestamp = write_timestamp; - spdif_pending_frames += frames_sent; - } - - bool decimation_reached = (spdif_dma_event_count >= SPDIF_DMA_EVENTS_PER_CALLBACK); - // Partial blocks mark an end-of-stream boundary (silence-padded tail). Fire immediately - // so the back-shifted timestamp isn't overwritten by a later full audio block landing - // in the same decimation window. - bool partial_flush = (real_frames > 0 && real_frames < SPDIF_BLOCK_SAMPLES); - - if (decimation_reached || partial_flush) { - if (spdif_pending_frames > 0) { - this->audio_output_callback_(spdif_pending_frames, spdif_pending_timestamp); - spdif_pending_frames = 0; - } - spdif_dma_event_count = 0; - } - } - if (lockstep_broken) { - ESP_LOGV(TAG, "Exiting: lockstep desync, restarting task"); - break; - } - - // Always-fill: produce exactly one SPDIF block this iteration. The blocking encoder write - // paces the task at the DMA consumption rate. - uint32_t real_frames_in_block = 0; - bool block_committed = false; - bool partial_write_failure = false; - - if (!this->pause_state_) { - while (real_frames_in_block < SPDIF_BLOCK_SAMPLES) { - if (audio_source->available() == 0) { - size_t bytes_read = audio_source->fill(read_timeout_ticks, false); - if (bytes_read == 0) { - break; // No upstream data within the read budget; silence-pad the remainder. - } - uint8_t *new_data = audio_source->mutable_data(); - this->apply_software_volume_(new_data, bytes_read); - this->swap_esp32_mono_samples_(new_data, bytes_read); - } - - const uint32_t frames_still_needed = SPDIF_BLOCK_SAMPLES - real_frames_in_block; - const size_t bytes_still_needed = this->current_stream_info_.frames_to_bytes(frames_still_needed); - const size_t bytes_to_feed = std::min(audio_source->available(), bytes_still_needed); - - uint32_t blocks_sent = 0; - size_t pcm_consumed = 0; - esp_err_t err = this->spdif_encoder_->write(audio_source->data(), bytes_to_feed, write_timeout_ticks, - &blocks_sent, &pcm_consumed); - if (err != ESP_OK) { - // A failed (or timed-out) send leaves an unsent block in the encoder's stitch buffer; - // resuming would credit the next iteration's bytes against an old block. Bail and - // let loop() restart the task with a clean encoder. - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE); - partial_write_failure = true; - break; - } - - if (pcm_consumed > 0) { - audio_source->consume(pcm_consumed); - real_frames_in_block += this->current_stream_info_.bytes_to_frames(pcm_consumed); - } - if (blocks_sent > 0) { - block_committed = true; - break; - } - } - } - - if (partial_write_failure) { - break; - } - - if (!block_committed) { - // Pad whatever real audio we managed to feed (if any) with silence to complete one block, - // or emit a full silence block if the encoder is empty. - esp_err_t err = this->spdif_encoder_->flush_with_silence(write_timeout_ticks); - if (err != ESP_OK) { - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE); - break; - } - } - - // One block committed to DMA; push exactly one record carrying its real-audio frame count. - // Failure here means the records queue is full, which violates the lockstep invariant. - if (xQueueSendToBack(this->write_records_queue_, &real_frames_in_block, 0) != pdTRUE) { - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC); - break; - } - - // Silence-timeout tracking and graceful-stop reset. - if (real_frames_in_block == 0) { - if (this->spdif_silence_start_ == 0) { - this->spdif_silence_start_ = millis(); - } - - if (this->timeout_.has_value()) { - const uint32_t silence_duration = millis() - this->spdif_silence_start_; - if (silence_duration >= this->timeout_.value()) { - ESP_LOGV(TAG, "Silence timeout reached (%" PRIu32 "ms) - stopping speaker", silence_duration); - break; - } - } - } else if (this->spdif_silence_start_ != 0) { - uint32_t silence_duration = millis() - this->spdif_silence_start_; - if (silence_duration > 100) { - ESP_LOGV(TAG, "Exiting silence mode after %" PRIu32 "ms, have audio data", silence_duration); - } - this->spdif_silence_start_ = 0; - } - } - } - - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING); - - // Reset SPDIF encoder state to prevent stale state on next start - if (this->spdif_encoder_ != nullptr) { - this->spdif_encoder_->set_preload_mode(false); - this->spdif_encoder_->reset(); - } - - audio_source.reset(); - - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPED); - - while (true) { - // Continuously delay until the loop method deletes the task - vTaskDelay(pdMS_TO_TICKS(10)); - } -} - -esp_err_t I2SAudioSpeakerSPDIF::start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) { - this->current_stream_info_ = audio_stream_info; - // SPDIF never narrows the bit depth; the encoder consumes the input format directly. - this->output_stream_info_ = audio_stream_info; - - // SPDIF mode validation - if (this->sample_rate_ != audio_stream_info.get_sample_rate()) { - ESP_LOGE(TAG, "Only supports a single sample rate (configured: %" PRIu32 " Hz, stream: %" PRIu32 " Hz)", - this->sample_rate_, audio_stream_info.get_sample_rate()); - return ESP_ERR_NOT_SUPPORTED; - } - const uint8_t bits_per_sample = audio_stream_info.get_bits_per_sample(); - if (bits_per_sample != 16 && bits_per_sample != 24 && bits_per_sample != 32) { - ESP_LOGE(TAG, "Only supports 16, 24, or 32 bits per sample (got %u)", (unsigned) bits_per_sample); - return ESP_ERR_NOT_SUPPORTED; - } - if (audio_stream_info.get_channels() != 2) { - ESP_LOGE(TAG, "Only supports stereo (2 channels)"); - return ESP_ERR_NOT_SUPPORTED; - } - - // Tell the encoder what input width to expect. 32-bit input is truncated to 24-bit on the wire. - this->spdif_encoder_->set_bytes_per_sample(bits_per_sample / 8); - - if (!this->parent_->try_lock()) { - ESP_LOGE(TAG, "Parent bus is busy"); - return ESP_ERR_INVALID_STATE; - } - - i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT; - -#if SOC_CLK_APLL_SUPPORTED - if (this->use_apll_) { - clk_src = i2s_clock_src_t::I2S_CLK_SRC_APLL; - } -#endif // SOC_CLK_APLL_SUPPORTED - - // SPDIF mode: fixed configuration for BMC encoding - // For new driver, dma_frame_num is in I2S frames (8 bytes each for 32-bit stereo) - uint32_t dma_buffer_length = SPDIF_BLOCK_I2S_FRAMES; // One SPDIF block = 384 I2S frames = 3072 bytes - - // Log DMA configuration for debugging - ESP_LOGV(TAG, "I2S DMA config: %zu buffers x %lu frames = %lu bytes total", (size_t) SPDIF_DMA_BUFFERS_COUNT, - (unsigned long) dma_buffer_length, - (unsigned long) (SPDIF_DMA_BUFFERS_COUNT * dma_buffer_length * 8)); // 8 bytes per frame for 32-bit stereo - - i2s_chan_config_t chan_cfg = { - .id = this->parent_->get_port(), - .role = this->i2s_role_, - .dma_desc_num = SPDIF_DMA_BUFFERS_COUNT, - .dma_frame_num = dma_buffer_length, - .auto_clear = true, - .intr_priority = 3, - }; - - // SPDIF: double sample rate for BMC, 32-bit stereo, only data pin needed - i2s_std_clk_config_t clk_cfg = { - .sample_rate_hz = this->sample_rate_ * 2, - .clk_src = clk_src, - .mclk_multiple = this->mclk_multiple_, - }; - - i2s_std_slot_config_t slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO); - - i2s_std_gpio_config_t gpio_cfg = { - .mclk = GPIO_NUM_NC, - .bclk = GPIO_NUM_NC, - .ws = GPIO_NUM_NC, - .dout = this->dout_pin_, - .din = GPIO_NUM_NC, - .invert_flags = - { - .mclk_inv = false, - .bclk_inv = false, - .ws_inv = false, - }, - }; - - i2s_std_config_t std_cfg = { - .clk_cfg = clk_cfg, - .slot_cfg = slot_cfg, - .gpio_cfg = gpio_cfg, - }; - - esp_err_t err = this->init_i2s_channel_(chan_cfg, std_cfg, SPDIF_I2S_EVENT_QUEUE_COUNT); - if (err != ESP_OK) { - return err; - } - - // Channel is NOT enabled here. The speaker task will preload DMA buffers - // with SPDIF-encoded silence before enabling, ensuring the first data on - // the wire is valid SPDIF (not raw zeros from auto_clear) and preventing - // phantom DMA events before real audio data is available. - - return ESP_OK; -} - -} // namespace esphome::i2s_audio - -#endif // USE_ESP32 && USE_I2S_AUDIO_SPDIF_MODE diff --git a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_spdif.h b/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_spdif.h deleted file mode 100644 index ca77741..0000000 --- a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_spdif.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include "esphome/core/defines.h" - -#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_SPDIF_MODE) - -#include "i2s_audio_speaker.h" -#include "spdif_encoder.h" - -namespace esphome::i2s_audio { - -/// @brief SPDIF speaker implementation. -/// Encodes PCM audio into IEC 60958-3 S/PDIF bitstream using BMC encoding, -/// outputting through a single I2S data pin. Maintains continuous output -/// (silence when no audio) to keep SPDIF receivers synchronized. -class I2SAudioSpeakerSPDIF : public I2SAudioSpeakerBase { - public: - void setup() override; - void dump_config() override; - - size_t play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) override; - - protected: - void run_speaker_task() override; - esp_err_t start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) override; - void on_task_stopped() override; - - SPDIFEncoder *spdif_encoder_{nullptr}; - uint32_t spdif_silence_start_{0}; // Timestamp when silence mode started (0 = not in silence) -}; - -} // namespace esphome::i2s_audio - -#endif // USE_ESP32 && USE_I2S_AUDIO_SPDIF_MODE diff --git a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.cpp deleted file mode 100644 index fb08a06..0000000 --- a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ /dev/null @@ -1,427 +0,0 @@ -#include "i2s_audio_speaker.h" - -#ifdef USE_ESP32 - -#include -#include - -#include "esphome/components/audio/audio.h" -#include "esphome/components/audio/audio_transfer_buffer.h" - -#include "esphome/core/application.h" -#include "esphome/core/hal.h" -#include "esphome/core/log.h" - -#include "esp_timer.h" - -// esp-audio-libs -#include - -namespace esphome::i2s_audio { - -static const char *const TAG = "i2s_audio.speaker"; - -// Software volume control maps the user-facing [0.0, 1.0] range to a Q31 scale factor. -// Volumes in (0.0, 1.0) map linearly to a dB reduction in [-49.0, 0.0] dB. -static constexpr float SOFTWARE_VOLUME_MIN_DB = -49.0f; - -void I2SAudioSpeakerBase::setup() { - this->event_group_ = xEventGroupCreate(); - - if (this->event_group_ == nullptr) { - ESP_LOGE(TAG, "Event group creation failed"); - this->mark_failed(); - return; - } - - // Initialize volume control. When audio_dac is configured, this sets the DAC volume. - // When no audio_dac is configured, this initializes software volume control. - this->set_volume(this->volume_); -} - -void I2SAudioSpeakerBase::dump_config() { - ESP_LOGCONFIG(TAG, - "Speaker:\n" - " Pin: %d\n" - " Buffer duration: %" PRIu32, - static_cast(this->dout_pin_), this->buffer_duration_ms_); - if (this->timeout_.has_value()) { - ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 " ms", this->timeout_.value()); - } -} - -void I2SAudioSpeakerBase::loop() { - uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); - - // A stop that arrives while stopped cancels any start that has not been processed yet - constexpr uint32_t stop_bits = SpeakerEventGroupBits::COMMAND_STOP | SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY; - if ((event_group_bits & stop_bits) && (this->state_ == speaker::STATE_STOPPED)) { - xEventGroupClearBits(this->event_group_, stop_bits | SpeakerEventGroupBits::COMMAND_START); - event_group_bits &= ~(stop_bits | SpeakerEventGroupBits::COMMAND_START); - } - - if ((event_group_bits & SpeakerEventGroupBits::COMMAND_START) && (this->state_ == speaker::STATE_STOPPED)) { - this->state_ = speaker::STATE_STARTING; - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); - } - - // Handle the task's state - if (event_group_bits & SpeakerEventGroupBits::TASK_STARTING) { - ESP_LOGD(TAG, "Starting"); - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING); - } - if (event_group_bits & SpeakerEventGroupBits::TASK_RUNNING) { - ESP_LOGV(TAG, "Started"); - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING); - this->state_ = speaker::STATE_RUNNING; - } - if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) { - ESP_LOGV(TAG, "Stopping"); - // Lockstep-breaking error bits are latched by the task and cleared along with all other bits - // when TASK_STOPPED is processed; log them here, exactly once, as the task winds down. - if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) { - ESP_LOGE(TAG, "ISR event queue overflow, restarting speaker task to recover timestamp sync"); - } - if (event_group_bits & SpeakerEventGroupBits::ERR_PARTIAL_WRITE) { - ESP_LOGE(TAG, "Partial DMA write broke buffer alignment, restarting speaker task"); - } - if (event_group_bits & SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC) { - ESP_LOGE(TAG, "Event/record queues desynced, restarting speaker task"); - } - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING); - this->state_ = speaker::STATE_STOPPING; - } - if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPED) { - ESP_LOGD(TAG, "Stopped"); - - vTaskDelete(this->speaker_task_handle_); - this->speaker_task_handle_ = nullptr; - - this->stop_i2s_driver_(); - // ALL_BITS includes COMMAND_START. Take the bits from the clear itself, not from the snapshot at - // the top of loop(): the audio source's task can raise a start at any point above, including - // during stop_i2s_driver_(), and nothing would ever re-issue it. - const EventBits_t bits_before_clear = xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); - if (bits_before_clear & SpeakerEventGroupBits::COMMAND_START) { - ESP_LOGD(TAG, "Start requested while stopping; keeping the request"); - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); - } - this->status_clear_error(); - - this->on_task_stopped(); - - this->state_ = speaker::STATE_STOPPED; - } - - if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NO_MEM) { - ESP_LOGE(TAG, "Speaker task setup failed (allocation, preload, or channel enable)"); - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); - } - - // Handle the speaker's state - switch (this->state_) { - case speaker::STATE_STARTING: - if (this->status_has_error()) { - break; - } - - // Still starting up or winding down from a previous run - if ((this->tx_handle_ != nullptr) || (this->speaker_task_handle_ != nullptr)) { - break; - } - - if (const esp_err_t err = this->start_i2s_driver(this->audio_stream_info_); err != ESP_OK) { - if (err != ESP_ERR_NOT_FINISHED) { - ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); - this->status_momentary_error("driver-failure", 1000); - } - break; - } - - xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, - &this->speaker_task_handle_); - - if (this->speaker_task_handle_ == nullptr) { - ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); - this->status_momentary_error("task-failure", 1000); - this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt - } - break; - case speaker::STATE_RUNNING: // Intentional fallthrough - case speaker::STATE_STOPPING: // Intentional fallthrough - case speaker::STATE_STOPPED: - break; - } -} - -void I2SAudioSpeakerBase::set_volume(float volume) { - this->volume_ = volume; -#ifdef USE_AUDIO_DAC - if (this->audio_dac_ != nullptr) { - if (volume > 0.0f) { - this->audio_dac_->set_mute_off(); - } - this->audio_dac_->set_volume(volume); - } else -#endif // USE_AUDIO_DAC - { - // Fallback to software volume control by using a Q31 fixed point scaling factor. - // At maximum volume (1.0), set to INT32_MAX to bypass volume processing entirely - // and avoid any floating-point precision issues that could cause slight volume reduction. - if (volume >= 1.0f) { - this->q31_volume_factor_ = INT32_MAX; - } else if (volume <= 0.0f) { - this->q31_volume_factor_ = 0; - } else { - this->q31_volume_factor_ = - esp_audio_libs::gain::db_to_q31(remap(volume, 0.0f, 1.0f, SOFTWARE_VOLUME_MIN_DB, 0.0f)); - } - } -} - -void I2SAudioSpeakerBase::set_mute_state(bool mute_state) { - this->mute_state_ = mute_state; -#ifdef USE_AUDIO_DAC - if (this->audio_dac_) { - if (mute_state) { - this->audio_dac_->set_mute_on(); - } else { - this->audio_dac_->set_mute_off(); - } - } else -#endif // USE_AUDIO_DAC - { - if (mute_state) { - // Fallback to software volume control and scale by 0 - this->q31_volume_factor_ = 0; - } else { - // Revert to previous volume when unmuting - this->set_volume(this->volume_); - } - } -} - -size_t I2SAudioSpeakerBase::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) { - if (this->is_failed()) { - ESP_LOGE(TAG, "Setup failed; cannot play audio"); - return 0; - } - - if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) { - this->start(); - } - - if (this->state_ != speaker::STATE_RUNNING) { - // Unable to write data to a running speaker, so delay the max amount of time so it can get ready - vTaskDelay(ticks_to_wait); - ticks_to_wait = 0; - } - - size_t bytes_written = 0; - if (this->state_ == speaker::STATE_RUNNING) { - std::shared_ptr temp_ring_buffer = this->audio_ring_buffer_.lock(); - if (temp_ring_buffer != nullptr) { - // The weak_ptr locks successfully only while the speaker task owns the ring buffer, so it is safe to write - bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait); - } - } - - return bytes_written; -} - -bool I2SAudioSpeakerBase::has_buffered_data() const { - std::shared_ptr temp_ring_buffer = this->audio_ring_buffer_.lock(); - if (temp_ring_buffer != nullptr) { - return temp_ring_buffer->available() > 0; - } - return false; -} - -void I2SAudioSpeakerBase::speaker_task(void *params) { - I2SAudioSpeakerBase *this_speaker = (I2SAudioSpeakerBase *) params; - this_speaker->run_speaker_task(); -} - -void I2SAudioSpeakerBase::start() { - if (!this->is_ready() || this->is_failed() || this->status_has_error()) - return; - if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING)) - return; - - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); -} - -void I2SAudioSpeakerBase::stop() { this->stop_(false); } - -void I2SAudioSpeakerBase::finish() { this->stop_(true); } - -void I2SAudioSpeakerBase::stop_(bool wait_on_empty) { - if (!this->is_ready() || this->is_failed()) - return; - - // Always set the bit, even when stopped, so loop() can cancel a start that is still pending - if (wait_on_empty) { - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY); - } else { - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP); - } -} - -esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg, - size_t event_queue_size) { - esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL); - if (err != ESP_OK) { - ESP_LOGE(TAG, "I2S channel allocation failed: %s", esp_err_to_name(err)); - this->parent_->unlock(); - return err; - } - - err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to initialize I2S channel"); - i2s_del_channel(this->tx_handle_); - this->tx_handle_ = nullptr; - this->parent_->unlock(); - return err; - } - - err = this->prepare_event_queues_(event_queue_size); - if (err != ESP_OK) { - i2s_del_channel(this->tx_handle_); - this->tx_handle_ = nullptr; - this->parent_->unlock(); - } - return err; -} - -esp_err_t I2SAudioSpeakerBase::prepare_event_queues_(size_t event_queue_size) { - if (this->i2s_event_queue_ == nullptr) { - this->i2s_event_queue_ = xQueueCreate(event_queue_size, sizeof(int64_t)); - } else { - // Reset queue to clear any stale events from previous task - xQueueReset(this->i2s_event_queue_); - } - - // Lockstep records queue. One record per in-flight DMA buffer; sized to match the I2S event queue - // so a fully-saturated DMA pipeline cannot overflow either side before drain. - if (this->write_records_queue_ == nullptr) { - this->write_records_queue_ = xQueueCreate(event_queue_size, sizeof(uint32_t)); - } else { - xQueueReset(this->write_records_queue_); - } - - if (this->i2s_event_queue_ == nullptr || this->write_records_queue_ == nullptr) { - ESP_LOGE(TAG, "Failed to allocate I2S event queue(s)"); - return ESP_ERR_NO_MEM; - } - - return ESP_OK; -} - -#ifdef USE_I2S_AUDIO_FULL_DUPLEX -esp_err_t I2SAudioSpeakerBase::acquire_full_duplex_channel_(size_t event_queue_size) { - if (this->parent_->is_tx_in_use()) { - // Another speaker on the bus is playing; loop() retries until it releases the channel - ESP_LOGV(TAG, "Full duplex channel busy"); - return ESP_ERR_NOT_FINISHED; - } - this->tx_handle_ = this->parent_->acquire_tx_channel(); - if (this->tx_handle_ == nullptr) { - ESP_LOGE(TAG, "Full duplex channel unavailable"); - return ESP_ERR_INVALID_STATE; - } - - esp_err_t err = this->prepare_event_queues_(event_queue_size); - if (err != ESP_OK) { - this->parent_->release_tx_channel(); - this->tx_handle_ = nullptr; - } - return err; -} -#endif - -void I2SAudioSpeakerBase::stop_i2s_driver_() { -#ifdef USE_I2S_AUDIO_FULL_DUPLEX - if (this->parent_->is_full_duplex()) { - // The parent owns the channel pair and its pins, so only hand the TX channel back - if (this->tx_handle_ != nullptr) { - this->parent_->release_tx_channel(); - this->tx_handle_ = nullptr; - } - return; - } -#endif - - if (this->tx_handle_ != nullptr) { - i2s_channel_disable(this->tx_handle_); - i2s_del_channel(this->tx_handle_); - this->tx_handle_ = nullptr; - - // i2s_del_channel() leaves dout wired to this port's data-out signal in the GPIO matrix: it only - // clears an internal reservation mask, never the esp_rom_gpio_connect_out_signal() routing that - // setup installed. If another speaker reuses this port (shared bus), its audio still reaches our - // dout. Detach the pin and drive it low so a stale output stops driving downstream hardware: a - // SPDIF optical transmitter would otherwise stay lit, and an analog DAC would emit noise. - gpio_reset_pin(this->dout_pin_); - gpio_set_direction(this->dout_pin_, GPIO_MODE_OUTPUT); - gpio_set_level(this->dout_pin_, 0); - } - this->parent_->unlock(); -} - -bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) { - int64_t now = esp_timer_get_time(); - - BaseType_t need_yield1 = pdFALSE; - BaseType_t need_yield2 = pdFALSE; - BaseType_t need_yield3 = pdFALSE; - - I2SAudioSpeakerBase *this_speaker = (I2SAudioSpeakerBase *) user_ctx; - - if (xQueueIsQueueFullFromISR(this_speaker->i2s_event_queue_)) { - // Queue is full, so discard the oldest event. Once we drop a completion event, ``i2s_event_queue_`` - // and any per-buffer record queue maintained by the task are permanently desynced, so the task - // must restart to recover. Set both ERR_DROPPED_EVENT (so loop() can log it) and COMMAND_STOP - // (so the task bails immediately, closing the race where loop() could clear the error bit - // before the task observes it). - int64_t dummy; - xQueueReceiveFromISR(this_speaker->i2s_event_queue_, &dummy, &need_yield1); - xEventGroupSetBitsFromISR(this_speaker->event_group_, - SpeakerEventGroupBits::ERR_DROPPED_EVENT | SpeakerEventGroupBits::COMMAND_STOP, - &need_yield2); - } - - xQueueSendToBackFromISR(this_speaker->i2s_event_queue_, &now, &need_yield3); - - return need_yield1 | need_yield2 | need_yield3; -} - -void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) { - if (this->q31_volume_factor_ == INT32_MAX) { - return; // Max volume, no processing needed - } - - const size_t bytes_per_sample = this->current_stream_info_.samples_to_bytes(1); - const uint32_t len = bytes_read / bytes_per_sample; - - esp_audio_libs::gain::apply(data, data, this->q31_volume_factor_, len, bytes_per_sample); -} - -void I2SAudioSpeakerBase::swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read) { -#ifdef USE_ESP32_VARIANT_ESP32 - // For ESP32 16-bit mono mode, adjacent samples need to be swapped. - if (this->output_stream_info_.get_channels() == 1 && this->output_stream_info_.get_bits_per_sample() == 16) { - int16_t *samples = reinterpret_cast(data); - size_t sample_count = bytes_read / sizeof(int16_t); - for (size_t i = 0; i + 1 < sample_count; i += 2) { - int16_t tmp = samples[i]; - samples[i] = samples[i + 1]; - samples[i + 1] = tmp; - } - } -#endif // USE_ESP32_VARIANT_ESP32 -} - -} // namespace esphome::i2s_audio - -#endif // USE_ESP32 diff --git a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.h b/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.h deleted file mode 100644 index 5f60e24..0000000 --- a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ /dev/null @@ -1,184 +0,0 @@ -#pragma once - -#ifdef USE_ESP32 - -#include "../i2s_audio.h" - -#include -#include -#include - -#include "esphome/components/audio/audio.h" -#include "esphome/components/ring_buffer/ring_buffer.h" -#include "esphome/components/speaker/speaker.h" - -#include "esphome/core/component.h" -#include "esphome/core/gpio.h" -#include "esphome/core/helpers.h" - -namespace esphome::i2s_audio { - -// Shared constants used by both standard and SPDIF speaker implementations -static constexpr size_t TASK_STACK_SIZE = 4096; -static constexpr ssize_t TASK_PRIORITY = 19; - -enum SpeakerEventGroupBits : uint32_t { - COMMAND_START = (1 << 0), // indicates loop should start speaker task - COMMAND_STOP = (1 << 1), // stops the speaker task - COMMAND_STOP_GRACEFULLY = (1 << 2), // Stops the speaker task once all data has been written - - TASK_STARTING = (1 << 10), - TASK_RUNNING = (1 << 11), - TASK_STOPPING = (1 << 12), - TASK_STOPPED = (1 << 13), - - ERR_ESP_NO_MEM = (1 << 19), - - ERR_DROPPED_EVENT = (1 << 20), // ISR overflowed the event queue, dropping a completion event - ERR_PARTIAL_WRITE = (1 << 21), // i2s_channel_write returned fewer bytes than requested - ERR_LOCKSTEP_DESYNC = (1 << 22), // i2s_event_queue_ and write_records_queue_ fell out of sync - - ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits -}; - -/// @brief Abstract base class for I2S audio speaker implementations. -/// Provides shared infrastructure (event groups, ring buffer, volume control, task lifecycle) -/// for derived standard I2S and SPDIF speaker classes. -class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public Component { - public: - float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; } - - void setup() override; - void dump_config() override; - void loop() override; - - 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_ = (gpio_num_t) pin; } - - /// @brief Get the I2S TX channel handle - i2s_chan_handle_t get_tx_handle() const { return this->tx_handle_; } - - void start() override; - void stop() override; - void finish() override; - - void set_pause_state(bool pause_state) override { this->pause_state_ = pause_state; } - bool get_pause_state() const override { return this->pause_state_; } - - /// @brief Plays the provided audio data. - /// Starts the speaker task, if necessary. Writes the audio data to the ring buffer. - /// @param data Audio data in the format set by the parent speaker classes ``set_audio_stream_info`` method. - /// @param length The length of the audio data in bytes. - /// @param ticks_to_wait The FreeRTOS ticks to wait before writing as much data as possible to the ring buffer. - /// @return The number of bytes that were actually written to the ring buffer. - size_t play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) override; - size_t play(const uint8_t *data, size_t length) override { return play(data, length, 0); } - - bool has_buffered_data() const override; - - /// @brief Sets the volume of the speaker. Uses the speaker's configured audio dac component. If unavailble, it is - /// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a - /// Q15 fixed-point factor. - /// @param volume between 0.0 and 1.0 - void set_volume(float volume) override; - - /// @brief Mutes or unmute the speaker. Uses the speaker's configured audio dac component. If unavailble, it is - /// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a - /// Q15 fixed-point factor. - /// @param mute_state true for muting, false for unmuting - void set_mute_state(bool mute_state) override; - - protected: - /// @brief FreeRTOS task entry point. Casts params to I2SAudioSpeakerBase and calls run_speaker_task_(). - /// @param params I2SAudioSpeakerBase component pointer - static void speaker_task(void *params); - - /// @brief The main speaker task loop. Implemented by derived classes for mode-specific behavior. - virtual void run_speaker_task() = 0; - - /// @brief Sends a stop command to the speaker task via ``event_group_``. - /// @param wait_on_empty If false, sends the COMMAND_STOP signal. If true, sends the COMMAND_STOP_GRACEFULLY signal. - void stop_(bool wait_on_empty); - - /// @brief Callback function used to send playback timestamps to the speaker task. - /// @param handle (i2s_chan_handle_t) - /// @param event (i2s_event_data_t) - /// @param user_ctx (void*) User context pointer that the callback accesses - /// @return True if a higher priority task was interrupted - static bool i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx); - - /// @brief Starts the ESP32 I2S driver. Implemented by derived classes for mode-specific configuration. - /// @param audio_stream_info Stream information for the I2S driver. - /// @return ESP_OK if successful, or an error code - virtual esp_err_t start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) = 0; - - /// @brief Shared I2S channel allocation, initialization, and event queue setup. - /// Called by derived start_i2s_driver_() implementations after building mode-specific configs. - /// @param chan_cfg I2S channel configuration - /// @param std_cfg I2S standard mode configuration (clock, slot, GPIO) - /// @param event_queue_size Size of the event queue - /// @return ESP_OK if successful, or an error code. On failure, cleans up channel and unlocks parent. - esp_err_t init_i2s_channel_(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg, - size_t event_queue_size); - - /// @brief Creates the lockstep queues, or empties them if they already exist. - /// @param event_queue_size Size of each queue - /// @return ESP_OK if successful, or ESP_ERR_NO_MEM - esp_err_t prepare_event_queues_(size_t event_queue_size); - -#ifdef USE_I2S_AUDIO_FULL_DUPLEX - /// @brief Takes the TX channel of the parent's full duplex pair and prepares the event queues. - /// @param event_queue_size Size of the event queue - /// @return ESP_OK if successful, or an error code. On failure, hands the channel back to the parent. - esp_err_t acquire_full_duplex_channel_(size_t event_queue_size); -#endif - - /// @brief Stops the I2S driver and unlocks the I2S port - void stop_i2s_driver_(); - - /// @brief Called in loop() when the task has stopped. Override for mode-specific cleanup. - virtual void on_task_stopped() {} - - /// @brief Apply software volume control using Q15 fixed-point scaling. - /// @param data Pointer to audio sample data (modified in place) - /// @param bytes_read Number of bytes of audio data - void apply_software_volume_(uint8_t *data, size_t bytes_read); - - /// @brief Swap adjacent 16-bit mono samples for ESP32 (non-variant) hardware quirk. - /// Only applies when running on original ESP32 with 16-bit mono output. Operates on the data that is - /// handed to the I2S peripheral, so the check uses the output (post-narrowing) stream info. - /// @param data Pointer to audio sample data (modified in place) - /// @param bytes_read Number of bytes of audio data - void swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read); - - TaskHandle_t speaker_task_handle_{nullptr}; - EventGroupHandle_t event_group_{nullptr}; - - // Lockstepped DMA buffer queues: i2s_event is outgoing, write_records is incoming - QueueHandle_t i2s_event_queue_{nullptr}; - QueueHandle_t write_records_queue_{nullptr}; - - std::weak_ptr audio_ring_buffer_; - - uint32_t buffer_duration_ms_; - - optional timeout_; - - bool pause_state_{false}; - - int32_t q31_volume_factor_{INT32_MAX}; - - audio::AudioStreamInfo current_stream_info_; // Format of the audio in the ring buffer (the I2S input) - // Format actually clocked out of the I2S peripheral. Same channel count and sample rate as - // current_stream_info_, but the bits per sample may be narrower when the incoming stream is wider than - // the speaker's configured slot bit width. Set by start_i2s_driver before the speaker task starts. - audio::AudioStreamInfo output_stream_info_; - - gpio_num_t dout_pin_; - i2s_chan_handle_t tx_handle_{nullptr}; -}; - -} // namespace esphome::i2s_audio - -#endif // USE_ESP32 diff --git a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp b/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp deleted file mode 100644 index 5e0624b..0000000 --- a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp +++ /dev/null @@ -1,528 +0,0 @@ -#include "i2s_audio_speaker_standard.h" - -#ifdef USE_ESP32 - -#include -#include - -#include "esphome/components/audio/audio.h" -#include "esphome/components/audio/audio_transfer_buffer.h" - -#include "esphome/core/hal.h" -#include "esphome/core/log.h" - -#include "esp_timer.h" - -// esp-audio-libs -#include - -namespace esphome::i2s_audio { - -static const char *const TAG = "i2s_audio.speaker.std"; - -static constexpr uint32_t DMA_BUFFER_DURATION_MS = 10; -static constexpr size_t DMA_BUFFERS_COUNT = 5; -// ESP-IDF clamps each DMA descriptor to this many bytes when allocating the channel (see i2s_get_buf_size in -// the I2S driver). Mirror its target-dependent selection so the requested dma_frame_num stays in range; the -// speaker task reads the size actually allocated back from the driver rather than relying on this value. -#if SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE -static constexpr size_t I2S_DMA_BUFFER_MAX_SIZE = DMA_DESCRIPTOR_BUFFER_MAX_SIZE_64B_ALIGNED; -#else -static constexpr size_t I2S_DMA_BUFFER_MAX_SIZE = DMA_DESCRIPTOR_BUFFER_MAX_SIZE_4B_ALIGNED; -#endif -// Sized to comfortably absorb scheduling jitter: at most DMA_BUFFERS_COUNT events can be in flight, -// doubled so that a transient backlog never overruns the queue (which would desync the lockstep -// invariant between i2s_event_queue_ and write_records_queue_). -static constexpr size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT * 2; -// Generous timeout for ``i2s_channel_write`` blocking. A buffer frees roughly every -// DMA_BUFFER_DURATION_MS, so a multiple of that gives plenty of slack against scheduling jitter -// without masking real failures. -static constexpr TickType_t WRITE_TIMEOUT_TICKS = pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * (DMA_BUFFERS_COUNT + 1)); - -// Requested frames per DMA buffer for the given stream, clamped so the byte size stays within the ESP-IDF -// maximum DMA descriptor size. This is only the value handed to the channel config: ESP-IDF may still adjust -// it (e.g. cache-line rounding on some targets), so the speaker task reads the size actually allocated back -// from the driver instead of assuming this value. Clamping here keeps the request in range and avoids a -// noisy ESP-IDF "dma frame num is out of dma buffer size" warning at high sample rates or bit depths. -static uint32_t dma_buffer_frames(const audio::AudioStreamInfo &stream_info) { - const uint32_t frames_from_duration = stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS); - const uint32_t max_frames = I2S_DMA_BUFFER_MAX_SIZE / stream_info.frames_to_bytes(1); - return std::min(frames_from_duration, max_frames); -} - -void I2SAudioSpeaker::dump_config() { - I2SAudioSpeakerBase::dump_config(); - const char *fmt_str; - switch (this->i2s_comm_fmt_) { - case I2SCommFmt::PCM: - fmt_str = "pcm"; - break; - case I2SCommFmt::MSB: - fmt_str = "msb"; - break; - default: - fmt_str = "std"; - break; - } - ESP_LOGCONFIG(TAG, " Communication format: %s", fmt_str); - if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { - // The width of each I2S slot. It is also the narrowing ceiling: streams wider than this are narrowed to - // it. A stream narrower than the slot is left at its own width and clocked into the wider slot, so this - // is not necessarily the sample data width (which depends on the incoming stream). - ESP_LOGCONFIG(TAG, " Slot bit width: %u", (unsigned) static_cast(this->slot_bit_width_)); - } -} - -void I2SAudioSpeaker::run_speaker_task() { - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING); - - const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT; - // Ensure ring buffer duration is at least the duration of all DMA buffers - const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this->buffer_duration_ms_); - - // The ring buffer holds input-format audio (what play() receives), so size it from the input stream info. - const size_t bytes_per_frame = this->current_stream_info_.frames_to_bytes(1); - // Round the ring buffer size down to a multiple of bytes_per_frame so the wrap boundary stays frame-aligned and - // avoids unnecessary single-frame splices. - const size_t ring_buffer_size = - (this->current_stream_info_.ms_to_bytes(ring_buffer_duration) / bytes_per_frame) * bytes_per_frame; - - // Per-frame byte widths and whether the task must narrow the bit depth before writing to the I2S peripheral. - const uint8_t channels = this->current_stream_info_.get_channels(); - const uint8_t input_bytes_per_sample = this->current_stream_info_.get_bits_per_sample() / 8; - const uint8_t output_bytes_per_sample = this->output_stream_info_.get_bits_per_sample() / 8; - const bool narrowing = input_bytes_per_sample != output_bytes_per_sample; - - // ESP-IDF may allocate smaller (or cache-line-rounded) DMA buffers than dma_buffer_frames() requested: it - // clamps each descriptor to the max DMA descriptor size and, on targets that route internal memory through - // the L1 cache (e.g. ESP32-P4), rounds the buffer to the cache line. Read the size the driver actually - // allocated so preload, silence padding, and the write/event lockstep all match it exactly. The channel is - // in the READY state here because start_i2s_driver() initialized it before this task was created. - size_t dma_buffer_bytes; - i2s_chan_info_t chan_info; - if (i2s_channel_get_info(this->tx_handle_, &chan_info) == ESP_OK && chan_info.total_dma_buf_size > 0) { - // total_dma_buf_size spans all DMA_BUFFERS_COUNT descriptors and is an exact multiple of the count. - dma_buffer_bytes = chan_info.total_dma_buf_size / DMA_BUFFERS_COUNT; - } else { - // Should not happen for a READY channel; fall back to the requested size. - dma_buffer_bytes = this->output_stream_info_.frames_to_bytes(dma_buffer_frames(this->output_stream_info_)); - } - // dma_buffer_bytes counts output-format bytes; convert with the output stream info. - const uint32_t frames_per_dma_buffer = this->output_stream_info_.bytes_to_frames(dma_buffer_bytes); - // Soft cap for each source read: enough input-format bytes to fill one DMA buffer's worth of frames. - const size_t dma_buffer_input_bytes = this->current_stream_info_.frames_to_bytes(frames_per_dma_buffer); - - bool successful_setup = false; - - std::unique_ptr audio_source; - - // Pre-zeroed buffer used to silence-pad each DMA descriptor whenever real audio doesn't fully fill it. - RAMAllocator silence_allocator; - uint8_t *silence_buffer = silence_allocator.allocate(dma_buffer_bytes); - - if (silence_buffer != nullptr) { - memset(silence_buffer, 0, dma_buffer_bytes); - - std::shared_ptr temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); - audio_source = audio::RingBufferAudioSource::create(temp_ring_buffer, dma_buffer_input_bytes, - static_cast(bytes_per_frame)); - - if (audio_source != nullptr) { - // audio_source is nullptr if the ring buffer fails to allocate - this->audio_ring_buffer_ = temp_ring_buffer; - successful_setup = true; - } - } - - if (successful_setup) { - // Preload every DMA descriptor with silence and push a matching zero-real-frames record per buffer. - // This guarantees that every on_sent event has a corresponding write record from the start, so - // ``i2s_event_queue_`` and ``write_records_queue_`` stay in lockstep for the entire task lifetime. - for (size_t i = 0; i < DMA_BUFFERS_COUNT; i++) { - size_t bytes_loaded = 0; - esp_err_t err = i2s_channel_preload_data(this->tx_handle_, silence_buffer, dma_buffer_bytes, &bytes_loaded); - if (err != ESP_OK || bytes_loaded != dma_buffer_bytes) { - ESP_LOGV(TAG, "Failed to preload silence into DMA buffer %u (err=%d, loaded=%u)", (unsigned) i, (int) err, - (unsigned) bytes_loaded); - successful_setup = false; - break; - } - uint32_t zero_real_frames = 0; - if (xQueueSend(this->write_records_queue_, &zero_real_frames, 0) != pdTRUE) { - // Should never happen: the queue was just reset and is sized for DMA_BUFFERS_COUNT * 2 entries. - ESP_LOGV(TAG, "Failed to push preload write record"); - successful_setup = false; - break; - } - } - } - - if (successful_setup) { - // Register the on_sent callback BEFORE enabling the channel so the very first transmitted buffer - // generates a queued event that pairs with the first preloaded silence record. - const i2s_event_callbacks_t callbacks = {.on_sent = i2s_on_sent_cb}; - i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this); - - if (i2s_channel_enable(this->tx_handle_) != ESP_OK) { - ESP_LOGV(TAG, "Failed to enable I2S channel"); - successful_setup = false; - } - } - - if (!successful_setup) { - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); - } else { - bool stop_gracefully = false; - // Number of records currently in ``write_records_queue_`` that carry real audio. Used by graceful - // stop to wait until every real-audio buffer has been confirmed played by an ISR event. - uint32_t pending_real_buffers = 0; - uint32_t last_data_received_time = millis(); - - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING); - - // Main speaker task loop. Continues while: - // - Paused, OR - // - No timeout configured, OR - // - Timeout hasn't elapsed since last data - // - // Always-fill model: every iteration writes exactly one DMA buffer's worth, mixing real audio - // and silence padding as needed. The blocking ``i2s_channel_write`` paces the loop at the DMA - // consumption rate, and every buffer write is matched 1:1 with a record on ``write_records_queue_``. - // - // While paused, the real-audio fill is skipped and the entire DMA buffer is filled with silence; - // the same blocking ``i2s_channel_write`` provides natural pacing (one buffer per ~DMA_BUFFER_DURATION_MS), - // so the lockstep invariant is preserved without burning CPU. - while (this->pause_state_ || !this->timeout_.has_value() || - (millis() - last_data_received_time) <= this->timeout_.value()) { - uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); - - if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { - // COMMAND_STOP is set both by user-initiated stop() and by the ISR when it drops a completion - // event (paired with ERR_DROPPED_EVENT so loop() can distinguish the two cases). - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP); - ESP_LOGV(TAG, "Exiting: COMMAND_STOP received"); - break; - } - if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY) { - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY); - stop_gracefully = true; - } - - if (this->audio_stream_info_ != this->current_stream_info_) { - // Audio stream info changed, stop the speaker task so it will restart with the proper settings. - ESP_LOGV(TAG, "Exiting: stream info changed"); - break; - } - - // Drain ISR-stamped completion events. Each event corresponds 1:1 with a write_records_queue_ - // entry by construction (preloaded records at startup, plus exactly one record pushed per - // iteration alongside exactly one DMA-buffer-sized write). - int64_t write_timestamp; - bool lockstep_broken = false; - while (xQueueReceive(this->i2s_event_queue_, &write_timestamp, 0)) { - uint32_t real_frames = 0; - if (xQueueReceive(this->write_records_queue_, &real_frames, 0) != pdTRUE) { - // Should never happen: would indicate the lockstep invariant is broken. - ESP_LOGV(TAG, "Event without matching write record"); - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC); - lockstep_broken = true; - break; - } - if (real_frames > 0) { - pending_real_buffers--; - // Real audio is packed at the start of each DMA buffer with any silence padding on the - // tail, so the real audio finished playing earlier than the buffer-completion timestamp - // by the duration of the trailing zeros. - const uint32_t silence_frames = frames_per_dma_buffer - real_frames; - const int64_t adjusted_ts = - write_timestamp - this->current_stream_info_.frames_to_microseconds(silence_frames); - this->audio_output_callback_(real_frames, adjusted_ts); - } - } - if (lockstep_broken) { - break; - } - - // Graceful stop: exit only after the source's exposed chunk is drained, the underlying ring - // buffer has nothing left to hand over, and every real-audio buffer we submitted has been - // confirmed played. ``has_buffered_data()`` returns bytes still sitting in the ring buffer - // awaiting fill(). - if (stop_gracefully && audio_source->available() == 0 && !this->has_buffered_data() && - pending_real_buffers == 0) { - ESP_LOGV(TAG, "Exiting: graceful stop complete"); - break; - } - - // Compose exactly one DMA buffer's worth: drain as much real audio as the source currently - // exposes (may take multiple fill() calls when crossing a ring buffer wrap), then pad any - // remainder with silence. All writes pack into the next free DMA descriptor in order, so the - // descriptor ends up holding [real audio][silence padding]. ``bytes_written_total`` counts - // output-format bytes so it tracks how full the DMA buffer is regardless of any narrowing. - size_t bytes_written_total = 0; - uint32_t real_frames_total = 0; - bool partial_write_failure = false; - - if (!this->pause_state_) { - while (bytes_written_total < dma_buffer_bytes) { - size_t bytes_read = audio_source->fill(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS) / 2, false); - if (bytes_read > 0) { - // Apply volume at the input bit depth, before any narrowing, so the full precision is scaled. - uint8_t *new_data = audio_source->mutable_data() + audio_source->available() - bytes_read; - this->apply_software_volume_(new_data, bytes_read); - } - - // Convert as many whole frames as fit in the remaining DMA space, bounded by what the source - // currently exposes. Frame counts are shared between input and output; only the byte widths differ. - const uint32_t frames_available = this->current_stream_info_.bytes_to_frames(audio_source->available()); - const uint32_t frames_room = - this->output_stream_info_.bytes_to_frames(dma_buffer_bytes - bytes_written_total); - const uint32_t frames_to_write = std::min(frames_available, frames_room); - if (frames_to_write == 0) { - // Ring buffer has nothing more to hand over right now; pad the rest of this DMA buffer - // with silence so the lockstep invariant (one write per iteration) is preserved. - break; - } - - const size_t input_bytes = this->current_stream_info_.frames_to_bytes(frames_to_write); - const size_t output_bytes = this->output_stream_info_.frames_to_bytes(frames_to_write); - - uint8_t *chunk = audio_source->mutable_data(); - if (narrowing) { - // Narrow the bit depth in place: output exactly aliases input with the same channel count and a - // smaller width, which copy_frames handles as a single forward pass. Only the frames about to be - // consumed are overwritten, so any unprocessed tail stays intact for the next iteration. - esp_audio_libs::pcm_convert::copy_frames(chunk, chunk, input_bytes_per_sample, channels, - output_bytes_per_sample, channels, frames_to_write); - } - this->swap_esp32_mono_samples_(chunk, output_bytes); - - size_t bw = 0; - i2s_channel_write(this->tx_handle_, chunk, output_bytes, &bw, WRITE_TIMEOUT_TICKS); - if (bw != output_bytes) { - // A short real-audio write breaks DMA descriptor alignment for every subsequent event; - // the only safe recovery is to restart the task. - ESP_LOGV(TAG, "Partial real audio write: %u of %u bytes", (unsigned) bw, (unsigned) output_bytes); - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE); - partial_write_failure = true; - break; - } - audio_source->consume(input_bytes); - bytes_written_total += output_bytes; - real_frames_total += frames_to_write; - } - if (real_frames_total > 0) { - last_data_received_time = millis(); - } - } - - if (partial_write_failure) { - break; - } - - const size_t silence_bytes = dma_buffer_bytes - bytes_written_total; - if (silence_bytes > 0) { - size_t bw = 0; - i2s_channel_write(this->tx_handle_, silence_buffer, silence_bytes, &bw, WRITE_TIMEOUT_TICKS); - if (bw != silence_bytes) { - // Same descriptor-alignment hazard as a partial real-audio write. - ESP_LOGV(TAG, "Partial silence write: %u of %u bytes", (unsigned) bw, (unsigned) silence_bytes); - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE); - break; - } - } - - // Push the matching write record. Capacity headroom in I2S_EVENT_QUEUE_COUNT guarantees this - // succeeds even with a transient backlog of unprocessed events; if it ever fails the lockstep - // invariant is broken and every subsequent timestamp would be silently wrong, so bail. - if (xQueueSend(this->write_records_queue_, &real_frames_total, 0) != pdTRUE) { - ESP_LOGV(TAG, "Exiting: write records queue full"); - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC); - break; - } - if (real_frames_total > 0) { - pending_real_buffers++; - } - } - } - - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING); - - audio_source.reset(); - - if (silence_buffer != nullptr) { - silence_allocator.deallocate(silence_buffer, dma_buffer_bytes); - silence_buffer = nullptr; - } - - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPED); - - while (true) { - // Continuously delay until the loop method deletes the task - vTaskDelay(pdMS_TO_TICKS(10)); - } -} - -esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) { - this->current_stream_info_ = audio_stream_info; - - if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT - // Can't reconfigure I2S bus, so the sample rate must match the configured value - ESP_LOGE(TAG, "Incompatible stream settings"); - return ESP_ERR_NOT_SUPPORTED; - } - - // When the stream is wider than the configured slot bit width, the speaker task narrows each frame in place - // before handing it to the I2S peripheral. Compute the output format here so the driver, DMA buffers, and - // the task's conversion all agree on the clocked-out width. A stream no wider than the slot width is passed - // through unchanged (the slot may still be wider than the data, the existing behavior). - uint8_t output_bits_per_sample = audio_stream_info.get_bits_per_sample(); - if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { - const uint8_t configured_bits = static_cast(this->slot_bit_width_); - if (output_bits_per_sample > configured_bits) { - output_bits_per_sample = configured_bits; - } - } - this->output_stream_info_ = audio::AudioStreamInfo(output_bits_per_sample, audio_stream_info.get_channels(), - audio_stream_info.get_sample_rate()); - -#ifdef USE_ESP32_VARIANT_ESP32 - // The original ESP32 I2S peripheral stores each sample in a whole number of 16-bit words (a 24-bit sample - // occupies 4 bytes in the DMA buffer, an 8-bit sample 2 bytes), but ESPHome's audio pipeline packs samples - // tightly (3 bytes for 24-bit, 1 for 8-bit). The two layouts only line up when the bit depth is a multiple - // of 16. The check is on the output width since that is what reaches the peripheral; a wider input is fine - // as long as it narrows to a 16- or 32-bit slot. - if (output_bits_per_sample % 16 != 0) { - ESP_LOGE(TAG, "ESP32 supports only 16- or 32-bit output, got %u-bit", (unsigned) output_bits_per_sample); - return ESP_ERR_NOT_SUPPORTED; - } -#endif // USE_ESP32_VARIANT_ESP32 - -#ifdef USE_I2S_AUDIO_FULL_DUPLEX - if (this->parent_->is_full_duplex()) { - // The parent set up the channel once from the configured format, so the stream must produce exactly that - const audio::AudioStreamInfo required = this->full_duplex_stream_info_(); - if (this->output_stream_info_ != required) { - ESP_LOGE(TAG, "Full duplex requires %u-bit, %u channel audio at %" PRIu32 " Hz", - (unsigned) required.get_bits_per_sample(), (unsigned) required.get_channels(), - required.get_sample_rate()); - return ESP_ERR_NOT_SUPPORTED; - } - // The speaker task will enable the channel after preloading. - return this->acquire_full_duplex_channel_(I2S_EVENT_QUEUE_COUNT); - } -#endif // USE_I2S_AUDIO_FULL_DUPLEX - - if (!this->parent_->try_lock()) { - ESP_LOGE(TAG, "Parent bus is busy"); - return ESP_ERR_INVALID_STATE; - } - - i2s_chan_config_t chan_cfg; - i2s_std_config_t std_cfg; - this->build_i2s_config_(this->output_stream_info_, chan_cfg, std_cfg); - - // The speaker task will enable the channel after preloading. - return this->init_i2s_channel_(chan_cfg, std_cfg, I2S_EVENT_QUEUE_COUNT); -} - -#ifdef USE_I2S_AUDIO_FULL_DUPLEX -audio::AudioStreamInfo I2SAudioSpeaker::full_duplex_stream_info_() const { - return audio::AudioStreamInfo(static_cast(this->slot_bit_width_), - this->slot_mode_ == I2S_SLOT_MODE_STEREO ? 2 : 1, this->sample_rate_); -} - -bool I2SAudioSpeaker::build_full_duplex_config(i2s_chan_config_t &chan_cfg, i2s_std_config_t &std_cfg) { - this->build_i2s_config_(this->full_duplex_stream_info_(), chan_cfg, std_cfg); - return true; -} -#endif // USE_I2S_AUDIO_FULL_DUPLEX - -void I2SAudioSpeaker::build_i2s_config_(const audio::AudioStreamInfo &output_stream_info, i2s_chan_config_t &chan_cfg, - i2s_std_config_t &std_cfg) const { - // The DMA buffers hold output-format (post-narrowing) samples, so size them from the output stream info. - uint32_t dma_buffer_length = dma_buffer_frames(output_stream_info); - - i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT; - -#if SOC_CLK_APLL_SUPPORTED - if (this->use_apll_) { - clk_src = i2s_clock_src_t::I2S_CLK_SRC_APLL; - } -#endif // SOC_CLK_APLL_SUPPORTED - - // Log DMA configuration for debugging - ESP_LOGV(TAG, "I2S DMA config: %zu buffers x %lu frames", (size_t) DMA_BUFFERS_COUNT, - (unsigned long) dma_buffer_length); - - 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, - .intr_priority = 3, - }; - - // Build standard I2S clock/slot/gpio configuration - i2s_std_clk_config_t clk_cfg = { - .sample_rate_hz = output_stream_info.get_sample_rate(), - .clk_src = clk_src, - .mclk_multiple = this->mclk_multiple_, - }; - - i2s_slot_mode_t slot_mode = this->slot_mode_; - i2s_std_slot_mask_t slot_mask = this->std_slot_mask_; - if (output_stream_info.get_channels() == 1) { - slot_mode = I2S_SLOT_MODE_MONO; - } else if (output_stream_info.get_channels() == 2) { - slot_mode = I2S_SLOT_MODE_STEREO; - slot_mask = I2S_STD_SLOT_BOTH; - } - - // Configure the data bit width from the output (post-narrowing) format, which is what is clocked out. - const i2s_data_bit_width_t data_bit_width = (i2s_data_bit_width_t) output_stream_info.get_bits_per_sample(); - i2s_std_slot_config_t slot_cfg; - switch (this->i2s_comm_fmt_) { - case I2SCommFmt::PCM: - slot_cfg = I2S_STD_PCM_SLOT_DEFAULT_CONFIG(data_bit_width, slot_mode); - break; - case I2SCommFmt::MSB: - slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(data_bit_width, slot_mode); - break; - default: - slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(data_bit_width, slot_mode); - break; - } - -#ifdef USE_ESP32_VARIANT_ESP32 - // There seems to be a bug on the ESP32 (non-variant) platform where setting the slot bit width higher than the - // bits per sample causes the audio to play too fast. Setting the ws_width to the configured slot bit width seems - // to make it play at the correct speed while sending more bits per slot. - if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { - uint32_t configured_bit_width = static_cast(this->slot_bit_width_); - slot_cfg.ws_width = configured_bit_width; - if (configured_bit_width > 16) { - slot_cfg.msb_right = false; - } - } -#else - slot_cfg.slot_bit_width = this->slot_bit_width_; - if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { - slot_cfg.ws_width = static_cast(this->slot_bit_width_); - } -#endif // USE_ESP32_VARIANT_ESP32 - slot_cfg.slot_mask = slot_mask; - - i2s_std_gpio_config_t gpio_cfg = this->parent_->get_pin_config(); - gpio_cfg.dout = this->dout_pin_; - - std_cfg = { - .clk_cfg = clk_cfg, - .slot_cfg = slot_cfg, - .gpio_cfg = gpio_cfg, - }; -} - -} // namespace esphome::i2s_audio - -#endif // USE_ESP32 diff --git a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.h b/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.h deleted file mode 100644 index cfa5bd8..0000000 --- a/Integrations/ESPHome/components/i2s_audio/speaker/i2s_audio_speaker_standard.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#ifdef USE_ESP32 - -#include "i2s_audio_speaker.h" - -namespace esphome::i2s_audio { - -enum class I2SCommFmt : uint8_t { - STANDARD, // Philips / I2S standard - PCM, // PCM short - MSB, // MSB / left-justified -}; - -/// @brief Standard I2S speaker implementation. -/// Outputs PCM audio data directly to an I2S DAC using the standard I2S protocol. -class I2SAudioSpeaker final : public I2SAudioSpeakerBase { - public: - void dump_config() override; - - void set_i2s_comm_fmt(I2SCommFmt fmt) { this->i2s_comm_fmt_ = fmt; } - -#ifdef USE_I2S_AUDIO_FULL_DUPLEX - bool build_full_duplex_config(i2s_chan_config_t &chan_cfg, i2s_std_config_t &std_cfg) override; -#endif - - protected: - void run_speaker_task() override; - esp_err_t start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) override; - - /// @brief Builds the channel and standard mode configuration for the given output format. - /// @param output_stream_info Format clocked out of the I2S peripheral - void build_i2s_config_(const audio::AudioStreamInfo &output_stream_info, i2s_chan_config_t &chan_cfg, - i2s_std_config_t &std_cfg) const; - -#ifdef USE_I2S_AUDIO_FULL_DUPLEX - /// @brief The output format of a full duplex bus, fixed by the configuration since the channel is set up once. - audio::AudioStreamInfo full_duplex_stream_info_() const; -#endif - - I2SCommFmt i2s_comm_fmt_{I2SCommFmt::STANDARD}; -}; - -} // namespace esphome::i2s_audio - -#endif // USE_ESP32 diff --git a/Integrations/ESPHome/components/i2s_audio/speaker/spdif_encoder.cpp b/Integrations/ESPHome/components/i2s_audio/speaker/spdif_encoder.cpp deleted file mode 100644 index 30146e0..0000000 --- a/Integrations/ESPHome/components/i2s_audio/speaker/spdif_encoder.cpp +++ /dev/null @@ -1,492 +0,0 @@ -#include "spdif_encoder.h" - -#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_SPDIF_MODE) - -#include "esphome/core/log.h" - -namespace esphome::i2s_audio { - -static const char *const TAG = "i2s_audio.spdif_encoder"; - -// S/PDIF preamble patterns (8 BMC bits each) -// These are the BMC-encoded sync patterns that violate normal BMC rules for easy detection. -// All preambles end at phase HIGH (last bit = 1), enabling consistent data encoding. -// Preamble is placed at bits 24-31 of word[0] for MSB-first transmission. -static constexpr uint8_t PREAMBLE_B = 0x17; // Block start (left channel, frame 0) -static constexpr uint8_t PREAMBLE_M = 0x1d; // Left channel (not block start) -static constexpr uint8_t PREAMBLE_W = 0x1b; // Right channel - -// BMC encoding of 4 zero bits starting at phase HIGH: 00_11_00_11 = 0x33 -// Used as a constant in the 16-bit subframe path, where bits 4-11 are always zero. -static constexpr uint32_t BMC_ZERO_NIBBLE = 0x33; - -// Constexpr BMC encoder for compile-time LUT generation. -// Encodes with start phase=true (HIGH). The complement property allows phase=false -// via XOR: bmc_encode(v, N, false) == bmc_encode(v, N, true) ^ mask -static constexpr uint16_t bmc_lut_encode(uint32_t data, uint8_t num_bits) { - uint16_t bmc = 0; - bool phase = true; - for (uint8_t i = 0; i < num_bits; i++) { - bool bit = (data >> i) & 1; - uint8_t bmc_pair = phase ? (bit ? 0b01 : 0b00) : (bit ? 0b10 : 0b11); - bmc |= static_cast(bmc_pair) << ((num_bits - 1 - i) * 2); - if (!bit) - phase = !phase; - } - return bmc; -} - -// Compile-time parity helper (constexpr-friendly, runs only at LUT build time). -static constexpr uint32_t bmc_lut_parity(uint32_t value, uint32_t num_bits) { - uint32_t p = 0; - for (uint32_t b = 0; b < num_bits; b++) - p ^= (value >> b) & 1u; - return p; -} - -// Combined BMC + phase-delta lookup tables. -// Each entry packs the BMC pattern (lower bits, phase=high start) together with -// a phase-mask delta in bits 16-31 (0xFFFF if the input has odd parity, else 0). -// XORing the delta into the running phase mask propagates parity across chunks -// without an explicit popcount. - -// 4-bit BMC lookup table: 16 entries x uint32_t = 64 bytes in flash. -// Bits 0-7 : 8-bit BMC pattern (phase=high start) -// Bits 16-31 : phase-mask delta (0xFFFFu if odd parity, else 0) -static constexpr auto BMC_LUT_4 = [] { - std::array t{}; - for (uint32_t i = 0; i < 16; i++) { - uint32_t bmc = bmc_lut_encode(i, 4); - uint32_t delta = bmc_lut_parity(i, 4) ? 0xFFFF0000u : 0u; - t[i] = bmc | delta; - } - return t; -}(); - -// 8-bit BMC lookup table: 256 entries x uint32_t = 1024 bytes in flash. -// Bits 0-15 : 16-bit BMC pattern (phase=high start) -// Bits 16-31 : phase-mask delta (0xFFFFu if odd parity, else 0) -static constexpr auto BMC_LUT_8 = [] { - std::array t{}; - for (uint32_t i = 0; i < 256; i++) { - uint32_t bmc = bmc_lut_encode(i, 8); - uint32_t delta = bmc_lut_parity(i, 8) ? 0xFFFF0000u : 0u; - t[i] = bmc | delta; - } - return t; -}(); - -// Initialize S/PDIF buffer -bool SPDIFEncoder::setup() { - this->spdif_block_buf_ = std::make_unique(SPDIF_BLOCK_SIZE_U32); - if (!this->spdif_block_buf_) { - ESP_LOGE(TAG, "Buffer allocation failed (%zu bytes)", SPDIF_BLOCK_SIZE_BYTES); - return false; - } - ESP_LOGV(TAG, "Buffer allocated (%zu bytes)", SPDIF_BLOCK_SIZE_BYTES); - - // Build initial channel status block with default sample rate and width - this->build_channel_status_(); - - this->reset(); - return true; -} - -void SPDIFEncoder::reset() { - this->spdif_block_ptr_ = this->spdif_block_buf_.get(); - this->frame_in_block_ = 0; - this->block_buf_is_silence_block_ = false; -} - -void SPDIFEncoder::set_sample_rate(uint32_t sample_rate) { - if (this->sample_rate_ != sample_rate) { - this->sample_rate_ = sample_rate; - this->build_channel_status_(); - ESP_LOGD(TAG, "Sample rate set to %lu Hz", (unsigned long) sample_rate); - } -} - -void SPDIFEncoder::set_bytes_per_sample(uint8_t bytes_per_sample) { - if (bytes_per_sample != 2 && bytes_per_sample != 3 && bytes_per_sample != 4) { - ESP_LOGE(TAG, "Unsupported bytes per sample: %u", (unsigned) bytes_per_sample); - return; - } - if (this->bytes_per_sample_ != bytes_per_sample) { - this->bytes_per_sample_ = bytes_per_sample; - this->build_channel_status_(); - // Discard any partial block built at the previous width so we never mix widths on the wire. - this->reset(); - ESP_LOGD(TAG, "Input width set to %u-bit", (unsigned) bytes_per_sample * 8); - } -} - -void SPDIFEncoder::build_channel_status_() { - // IEC 60958-3 Consumer Channel Status Block (192 bits = 24 bytes) - // Transmitted LSB-first within each byte, one bit per frame via C bit. - - // Any cached silence block was built for the previous channel status; it is now stale. - this->block_buf_is_silence_block_ = false; - - this->channel_status_.fill(0); - - // Byte 0: Consumer, PCM audio, no copyright, no pre-emphasis, Mode 0 - // All bits are 0, which is already set - - // Byte 1: Category code = 0x00 (general) - // Already 0 - - // Byte 2: Source/channel unspecified - // Already 0 - - // Byte 3: Sample frequency code (bits 0-3) + clock accuracy (bits 4-5) - // Clock accuracy = 00 (Level II, ±1000 ppm) - appropriate for ESP32 - uint8_t freq_code; - switch (this->sample_rate_) { - case 44100: - freq_code = 0x0; // 0000 - break; - case 48000: - freq_code = 0x2; // 0010 - break; - default: - // Other values are possible but they're not supported by ESPHome - freq_code = 0x1; // 0001 = not indicated - ESP_LOGW(TAG, "Unsupported sample rate %lu Hz, channel status will indicate 'not specified'", - (unsigned long) this->sample_rate_); - break; - } - // Byte 3: freq_code in bits 0-3, clock accuracy (00) in bits 4-5 - this->channel_status_[3] = freq_code; // Clock accuracy bits 4-5 are already 0 - - // Byte 4: Word length encoding (IEC 60958-3 consumer) - // bit 0: max length flag (0 = max 20 bits, 1 = max 24 bits) - // bits 1-3: word length code relative to the max - // For our supported widths: - // 16-bit (max 20): 0b0010 = 0x02 -- "16 bits, max 20" - // 24-bit (max 24): 0b1101 = 0x0D -- "24 bits, max 24" - // 32-bit input is truncated to 24-bit on the wire, so use the 24-bit code. - uint8_t word_length_code; - switch (this->bytes_per_sample_) { - case 2: - word_length_code = 0x02; - break; - case 3: // Shared case - case 4: - word_length_code = 0x0D; - break; - default: - word_length_code = 0x00; // not specified - break; - } - this->channel_status_[4] = word_length_code; -} - -// Extract the C bit for the given frame from channel_status_ and shift it into bit 30 -// so it can be OR'd directly into a raw subframe. -ESPHOME_ALWAYS_INLINE static inline uint32_t c_bit_for_frame(const std::array &channel_status, - uint32_t frame) { - return static_cast((channel_status[frame >> 3] >> (frame & 7)) & 1u) << 30; -} - -// ============================================================================ -// IEC 60958 subframe bit layout -// ============================================================================ -// Bits 0-3: Preamble (handled separately, not in raw_subframe) -// Bits 4-7: Auxiliary audio data / 24-bit audio LSB -// Bits 8-11: Audio LSB extension (zero for 16-bit, low nibble of audio for 24-bit) -// Bits 12-27: Audio sample (16 high bits in 16-bit mode, mid 16 bits in 24-bit mode) -// Bit 28: V (Validity) - 0 = valid audio -// Bit 29: U (User data) - 0 -// Bit 30: C (Channel status) - from channel status block -// Bit 31: P (Parity) - even parity over bits 4-31 -// ============================================================================ - -// Build a raw IEC 60958 subframe from PCM little-endian input of width Bps bytes. -// Caller is responsible for OR-ing in the C bit and parity. -template ESPHOME_ALWAYS_INLINE static inline uint32_t build_raw_subframe(const uint8_t *pcm_sample) { - static_assert(Bps == 2 || Bps == 3 || Bps == 4, "Unsupported bytes per sample"); - if constexpr (Bps == 2) { - // 16-bit input: MSB-aligned in the 20-bit audio field, bits 12-27. - return (static_cast(pcm_sample[1]) << 20) | (static_cast(pcm_sample[0]) << 12); - } else if constexpr (Bps == 3) { - // 24-bit input: full 24-bit audio field, bits 4-27. - return (static_cast(pcm_sample[2]) << 20) | (static_cast(pcm_sample[1]) << 12) | - (static_cast(pcm_sample[0]) << 4); - } else { // Bps == 4 - // 32-bit input truncated to 24-bit: drop the lowest byte. - return (static_cast(pcm_sample[3]) << 20) | (static_cast(pcm_sample[2]) << 12) | - (static_cast(pcm_sample[1]) << 4); - } -} - -// BMC-encode a subframe and write the two output uint32 words to dst. Caller passes -// raw_subframe with the C bit set (bit 30) and the P bit cleared (bit 31 = 0). P is -// derived from the cumulative parity-mask delta of the per-byte LUT lookups. -// -// I2S halfword swap means word[0] transmits as: bits 24-31, 16-23, 8-15, 0-7. -// word[1] transmits as: bits 16-31, 0-15. Within each halfword, MSB-first. -// All preambles end at phase HIGH, so phase=true at the start of bit 4. -// -// P-bit derivation: BMC_LUT_*'s upper half encodes the parity of the input chunk. Each -// chunk's parity delta is shifted down (`lut >> 16`) into a phase_mask that lives in the -// low 16 bits, so the same value can also be XORed against subsequent BMC patterns to -// invert phase. XOR'ing those deltas through all chunks (with bit 31 = 0) yields the -// parity of bits 4-30 in the low bits of phase_mask -- the required value of the P bit -// for even total parity. The BMC of bit 31 lives in bit 0 of the high-byte BMC output -// (i = 7 maps to position (8-1-7)*2 = 0); flipping the source bit flips only the lower -// BMC bit (= phase XOR bit), so applying P is `bmc_24_31 ^= phase_mask & 1u`. -template -ESPHOME_ALWAYS_INLINE static inline void bmc_encode_subframe(uint32_t raw_subframe, uint8_t preamble, uint32_t *dst) { - if constexpr (Bps == 2) { - // 16-bit path: bits 4-11 are zero, encoded inline as BMC_ZERO_NIBBLE constants. - // Eight zero source bits with start phase=HIGH end at phase=HIGH (popcount of zeros is even), - // so encoding of bits 12-15 starts at phase=true. Zeros contribute 0 to parity. - uint32_t nibble = (raw_subframe >> 12) & 0xF; - uint32_t lut_n = BMC_LUT_4[nibble]; - uint32_t bmc_12_15 = lut_n & 0xFFu; - uint32_t phase_mask = lut_n >> 16; // 0xFFFFu if odd parity, else 0 - - uint32_t byte_mid = (raw_subframe >> 16) & 0xFF; - uint32_t lut_m = BMC_LUT_8[byte_mid]; - uint32_t bmc_16_23 = (lut_m & 0xFFFFu) ^ phase_mask; - phase_mask ^= lut_m >> 16; - - uint32_t byte_hi = (raw_subframe >> 24) & 0xFF; // bit 7 (= P) is 0 by precondition - uint32_t lut_h = BMC_LUT_8[byte_hi]; - uint32_t bmc_24_31 = (lut_h & 0xFFFFu) ^ phase_mask; - phase_mask ^= lut_h >> 16; - // phase_mask now reflects parity of bits 4-30. Apply P by flipping bit 0 of bmc_24_31. - bmc_24_31 ^= phase_mask & 1u; - - dst[0] = bmc_12_15 | (BMC_ZERO_NIBBLE << 8) | (BMC_ZERO_NIBBLE << 16) | (static_cast(preamble) << 24); - dst[1] = bmc_24_31 | (bmc_16_23 << 16); - } else { - // 24-bit (and 32-bit truncated) path: bits 4-11 are live audio. - uint32_t byte_lo = (raw_subframe >> 4) & 0xFF; - uint32_t lut_l = BMC_LUT_8[byte_lo]; - uint32_t bmc_4_11 = lut_l & 0xFFFFu; - uint32_t phase_mask = lut_l >> 16; // 0xFFFFu if odd parity, else 0 - - uint32_t nibble = (raw_subframe >> 12) & 0xF; - uint32_t lut_n = BMC_LUT_4[nibble]; - uint32_t bmc_12_15 = (lut_n & 0xFFu) ^ (phase_mask & 0xFFu); - phase_mask ^= lut_n >> 16; - - uint32_t byte_mid = (raw_subframe >> 16) & 0xFF; - uint32_t lut_m = BMC_LUT_8[byte_mid]; - uint32_t bmc_16_23 = (lut_m & 0xFFFFu) ^ phase_mask; - phase_mask ^= lut_m >> 16; - - uint32_t byte_hi = (raw_subframe >> 24) & 0xFF; // bit 7 (= P) is 0 by precondition - uint32_t lut_h = BMC_LUT_8[byte_hi]; - uint32_t bmc_24_31 = (lut_h & 0xFFFFu) ^ phase_mask; - phase_mask ^= lut_h >> 16; - bmc_24_31 ^= phase_mask & 1u; - - // word[0]: bits 24-31 = preamble, bits 8-23 = bmc(4-11), bits 0-7 = bmc(12-15) - // word[1]: bits 16-31 = bmc(16-23), bits 0-15 = bmc(24-31) - dst[0] = bmc_12_15 | (bmc_4_11 << 8) | (static_cast(preamble) << 24); - dst[1] = bmc_24_31 | (bmc_16_23 << 16); - } -} - -template void SPDIFEncoder::encode_silence_frame_() { - static constexpr uint8_t SILENCE[4] = {0, 0, 0, 0}; - uint32_t raw = build_raw_subframe(SILENCE) | c_bit_for_frame(this->channel_status_, this->frame_in_block_); - uint8_t preamble_l = (this->frame_in_block_ == 0) ? PREAMBLE_B : PREAMBLE_M; - bmc_encode_subframe(raw, preamble_l, this->spdif_block_ptr_); - bmc_encode_subframe(raw, PREAMBLE_W, this->spdif_block_ptr_ + 2); - this->spdif_block_ptr_ += 4; - if (++this->frame_in_block_ >= SPDIF_BLOCK_SAMPLES) { - this->frame_in_block_ = 0; - } -} - -esp_err_t SPDIFEncoder::send_block_(TickType_t ticks_to_wait) { - // Use the appropriate callback and context based on preload mode - SPDIFBlockCallback callback; - void *ctx; - - if (this->preload_mode_) { - callback = this->preload_callback_; - ctx = this->preload_callback_ctx_; - } else { - callback = this->write_callback_; - ctx = this->write_callback_ctx_; - } - - if (callback == nullptr) { - return ESP_ERR_INVALID_STATE; - } - - esp_err_t err = callback(ctx, this->spdif_block_buf_.get(), SPDIF_BLOCK_SIZE_BYTES, ticks_to_wait); - - if (err == ESP_OK) { - // Reset pointer for next block; position tracking continues from where it left off - this->spdif_block_ptr_ = this->spdif_block_buf_.get(); - } - - return err; -} - -template -HOT esp_err_t SPDIFEncoder::write_typed_(const uint8_t *src, size_t size, TickType_t ticks_to_wait, - uint32_t *blocks_sent, size_t *bytes_consumed) { - const uint8_t *pcm_data = src; - const uint8_t *const pcm_end = src + size; - uint32_t block_count = 0; - - // Hot state lives in locals so the compiler can keep it in registers across the - // per-frame encoding work; byte writes through block_ptr may alias the member fields, - // which would block register allocation if the encoding read them directly from this->*. - uint32_t *block_ptr = this->spdif_block_ptr_; - uint32_t *const block_buf = this->spdif_block_buf_.get(); - uint32_t *const block_end = block_buf + SPDIF_BLOCK_SIZE_U32; - uint32_t frame = this->frame_in_block_; - const std::array &channel_status = this->channel_status_; - - auto save_state = [&]() { - this->spdif_block_ptr_ = block_ptr; - this->frame_in_block_ = static_cast(frame); - }; - - auto report_out_params = [&]() { - if (blocks_sent != nullptr) - *blocks_sent = block_count; - if (bytes_consumed != nullptr) - *bytes_consumed = pcm_data - src; - }; - - // Send a completed block if the buffer is full, propagating any error. - // send_block_ resets this->spdif_block_ptr_ to block_buf on success and leaves it - // unchanged on error -- mirror both behaviors in our local block_ptr. - auto maybe_send = [&]() -> esp_err_t { - if (block_ptr >= block_end) { - esp_err_t err = this->send_block_(ticks_to_wait); - if (err != ESP_OK) { - save_state(); - report_out_params(); - return err; - } - block_ptr = block_buf; - ++block_count; - } - return ESP_OK; - }; - - // Hot path: encode L+R pairs in two peeled sub-loops. Frame 0 carries the only - // buffer-full check and uses PREAMBLE_B (a block fills exactly when frame wraps from - // 191 back to 0). Frames 1..191 use PREAMBLE_M and need no buffer-full check or - // preamble branch. The encoding body is inlined here so block_ptr lives in a register - // for the duration of the loop. - while (pcm_data + 2 * Bps <= pcm_end) { - if (frame == 0) { - esp_err_t err = maybe_send(); - if (err != ESP_OK) - return err; - - uint32_t c_bit = c_bit_for_frame(channel_status, 0); - uint32_t raw_l = build_raw_subframe(pcm_data) | c_bit; - uint32_t raw_r = build_raw_subframe(pcm_data + Bps) | c_bit; - bmc_encode_subframe(raw_l, PREAMBLE_B, block_ptr); - bmc_encode_subframe(raw_r, PREAMBLE_W, block_ptr + 2); - block_ptr += 4; - frame = 1; - pcm_data += 2 * Bps; - } - - // The inner loop runs until min(SPDIF_BLOCK_SAMPLES, frame + input_frames). The - // input-size bound is folded into end_frame so a single `frame < end_frame` test - // governs termination. - uint32_t input_frames = static_cast(pcm_end - pcm_data) / (2u * Bps); - uint32_t end_frame = SPDIF_BLOCK_SAMPLES; - if (frame + input_frames < end_frame) - end_frame = frame + input_frames; - - while (frame < end_frame) { - uint32_t c_bit = c_bit_for_frame(channel_status, frame); - uint32_t raw_l = build_raw_subframe(pcm_data) | c_bit; - uint32_t raw_r = build_raw_subframe(pcm_data + Bps) | c_bit; - bmc_encode_subframe(raw_l, PREAMBLE_M, block_ptr); - bmc_encode_subframe(raw_r, PREAMBLE_W, block_ptr + 2); - block_ptr += 4; - ++frame; - pcm_data += 2 * Bps; - } - if (frame >= SPDIF_BLOCK_SAMPLES) - frame = 0; - } - - // Send any complete block that was just finished. - if (block_ptr >= block_end) { - esp_err_t err = this->send_block_(ticks_to_wait); - if (err != ESP_OK) { - save_state(); - report_out_params(); - return err; - } - block_ptr = block_buf; - ++block_count; - } - - save_state(); - report_out_params(); - return ESP_OK; -} - -HOT esp_err_t SPDIFEncoder::write(const uint8_t *src, size_t size, TickType_t ticks_to_wait, uint32_t *blocks_sent, - size_t *bytes_consumed) { - if (size > 0) { - // Real PCM is about to be encoded into the buffer, so it is no longer a full-silence block. - this->block_buf_is_silence_block_ = false; - } - switch (this->bytes_per_sample_) { - case 2: - return this->write_typed_<2>(src, size, ticks_to_wait, blocks_sent, bytes_consumed); - case 3: - return this->write_typed_<3>(src, size, ticks_to_wait, blocks_sent, bytes_consumed); - case 4: - return this->write_typed_<4>(src, size, ticks_to_wait, blocks_sent, bytes_consumed); - default: - return ESP_ERR_INVALID_STATE; - } -} - -template esp_err_t SPDIFEncoder::flush_with_silence_typed_(TickType_t ticks_to_wait) { - // If a complete block is already pending (from a previous failed send), emit just that block. - // Otherwise pad the partial block with silence (or generate a full silence block if empty) and - // send. Always emits exactly one block on success. - if (this->spdif_block_ptr_ < &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) { - const bool was_empty = (this->spdif_block_ptr_ == this->spdif_block_buf_.get()); - // Continuous-silence idle case: a full silence block is byte-identical every time for the - // active channel status, so when the buffer already holds one, re-send it as-is. - if (was_empty && this->block_buf_is_silence_block_) { - return this->send_block_(ticks_to_wait); - } - // Pad with silence frames at the configured width. - while (this->spdif_block_ptr_ < &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) { - this->encode_silence_frame_(); - } - // The buffer is a reusable full-silence block only if it was built entirely from silence; a - // partial real-audio block padded out with silence is not. - this->block_buf_is_silence_block_ = was_empty; - } - return this->send_block_(ticks_to_wait); -} - -esp_err_t SPDIFEncoder::flush_with_silence(TickType_t ticks_to_wait) { - switch (this->bytes_per_sample_) { - case 2: - return this->flush_with_silence_typed_<2>(ticks_to_wait); - case 3: - return this->flush_with_silence_typed_<3>(ticks_to_wait); - case 4: - return this->flush_with_silence_typed_<4>(ticks_to_wait); - default: - return ESP_ERR_INVALID_STATE; - } -} - -} // namespace esphome::i2s_audio - -#endif // USE_I2S_AUDIO_SPDIF_MODE diff --git a/Integrations/ESPHome/components/i2s_audio/speaker/spdif_encoder.h b/Integrations/ESPHome/components/i2s_audio/speaker/spdif_encoder.h deleted file mode 100644 index 9e23a85..0000000 --- a/Integrations/ESPHome/components/i2s_audio/speaker/spdif_encoder.h +++ /dev/null @@ -1,148 +0,0 @@ -#pragma once - -#include "esphome/core/defines.h" - -#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_SPDIF_MODE) - -#include -#include -#include -#include -#include "esp_err.h" -#include "esphome/core/helpers.h" - -namespace esphome::i2s_audio { - -// A SPDIF sample is 64-bits -static constexpr uint8_t SPDIF_BITS_PER_SAMPLE = 64; -// Number of samples in a SPDIF block -static constexpr uint16_t SPDIF_BLOCK_SAMPLES = 192; -// To emulate bi-phase mark code (BMC) (aka differential Manchester encoding) we send twice -// as many bits per sample so that we can generate the transitions this encoding requires. -static constexpr uint8_t EMULATED_BMC_BITS_PER_SAMPLE = SPDIF_BITS_PER_SAMPLE * 2; -static constexpr uint16_t SPDIF_BLOCK_SIZE_BYTES = SPDIF_BLOCK_SAMPLES * (EMULATED_BMC_BITS_PER_SAMPLE / 8); -static constexpr uint32_t SPDIF_BLOCK_SIZE_U32 = SPDIF_BLOCK_SIZE_BYTES / sizeof(uint32_t); // 3072 bytes / 4 = 768 -// I2S frame count for one SPDIF block (for new driver where frame = 8 bytes for 32-bit stereo) -static constexpr uint32_t SPDIF_BLOCK_I2S_FRAMES = SPDIF_BLOCK_SIZE_BYTES / 8; // 3072 / 8 = 384 frames - -/// Callback signature for block completion (raw function pointer for minimal overhead) -/// @param user_ctx User context pointer passed during callback registration -/// @param data Pointer to SPDIF encoded block data -/// @param size Size of the block in bytes (always SPDIF_BLOCK_SIZE_BYTES) -/// @param ticks_to_wait FreeRTOS ticks to wait for write completion -/// @return ESP_OK on success, or an error code -using SPDIFBlockCallback = esp_err_t (*)(void *user_ctx, uint32_t *data, size_t size, TickType_t ticks_to_wait); - -class SPDIFEncoder { - public: - /// @brief Initialize the SPDIF working buffer - /// @return true if setup was successful, false if allocation failed - bool setup(); - - /// @brief Set callback for normal writes (used when channel is running) - /// @param callback Function pointer to call when a block is ready - /// @param user_ctx Context pointer passed to callback (typically 'this' pointer of speaker) - void set_write_callback(SPDIFBlockCallback callback, void *user_ctx) { - this->write_callback_ = callback; - this->write_callback_ctx_ = user_ctx; - } - - /// @brief Set callback for preload writes (used when preloading to DMA before enabling channel) - /// @param callback Function pointer to call when a block is ready for preload - /// @param user_ctx Context pointer passed to callback (typically 'this' pointer of speaker) - void set_preload_callback(SPDIFBlockCallback callback, void *user_ctx) { - this->preload_callback_ = callback; - this->preload_callback_ctx_ = user_ctx; - } - - /// @brief Enable or disable preload mode - /// When in preload mode, completed blocks use the preload callback instead of write callback - void set_preload_mode(bool preload) { this->preload_mode_ = preload; } - - /// @brief Check if currently in preload mode - bool is_preload_mode() const { return this->preload_mode_; } - - /// @brief Set input PCM width: 2 = 16-bit, 3 = 24-bit, 4 = 32-bit (truncated to 24-bit on the wire). - /// Must be called before write() if input width changes from the default (16-bit). Triggers a - /// channel-status rebuild to reflect the new word length. - void set_bytes_per_sample(uint8_t bytes_per_sample); - - /// @brief Get the configured input PCM width in bytes per sample - uint8_t get_bytes_per_sample() const { return this->bytes_per_sample_; } - - /// @brief Convert PCM audio data to SPDIF BMC encoded data - /// @param src Source PCM audio data (stereo, width matches set_bytes_per_sample) - /// @param size Size of source data in bytes - /// @param ticks_to_wait Timeout for blocking writes - /// @param blocks_sent Optional pointer to receive the number of complete SPDIF blocks sent - /// @param bytes_consumed Optional pointer to receive the number of PCM bytes consumed from src - /// @return esp_err_t as returned from the callback - esp_err_t write(const uint8_t *src, size_t size, TickType_t ticks_to_wait, uint32_t *blocks_sent = nullptr, - size_t *bytes_consumed = nullptr); - - /// @brief Emit one complete SPDIF block: pad any pending partial block with silence and send, - /// or send a full silence block if nothing is pending. Always produces exactly one block on success. - /// @param ticks_to_wait Timeout for blocking writes - /// @return esp_err_t as returned from the callback - esp_err_t flush_with_silence(TickType_t ticks_to_wait); - - /// @brief Reset the SPDIF block buffer and position tracking, discarding any partial block - void reset(); - - /// @brief Set the sample rate for Channel Status Block encoding - /// @param sample_rate Sample rate in Hz (e.g., 44100, 48000) - /// Call this before writing audio data to ensure correct channel status. - void set_sample_rate(uint32_t sample_rate); - - /// @brief Get the currently configured sample rate - uint32_t get_sample_rate() const { return this->sample_rate_; } - - protected: - /// @brief Encode a single stereo silence frame at the current block position. - /// @note Used only by flush_with_silence_typed_ to pad; the hot write path inlines the - /// encoding body directly into write_typed_ to keep block_ptr / frame_in_block_ in registers. - template void encode_silence_frame_(); - - /// @brief Templated write loop. Called from the public write() via runtime dispatch on bytes_per_sample_. - template - HOT esp_err_t write_typed_(const uint8_t *src, size_t size, TickType_t ticks_to_wait, uint32_t *blocks_sent, - size_t *bytes_consumed); - - /// @brief Templated flush-with-silence. Pads the pending block with zeros at the configured width - /// (or builds a full silence block when nothing is pending) and sends it. Always emits one block. - template esp_err_t flush_with_silence_typed_(TickType_t ticks_to_wait); - - /// @brief Send the completed block via the appropriate callback - esp_err_t send_block_(TickType_t ticks_to_wait); - - /// @brief Build the channel status block from current configuration - void build_channel_status_(); - - // Member ordering optimized to minimize padding (largest alignment first) - - // 4-byte aligned members (pointers and uint32_t) - SPDIFBlockCallback write_callback_{nullptr}; - SPDIFBlockCallback preload_callback_{nullptr}; - void *write_callback_ctx_{nullptr}; - void *preload_callback_ctx_{nullptr}; - std::unique_ptr spdif_block_buf_; // Working buffer for SPDIF block (heap allocated) - uint32_t *spdif_block_ptr_{nullptr}; // Current position in block buffer - uint32_t sample_rate_{48000}; // Sample rate for Channel Status Block encoding - - // 1-byte aligned members (grouped together to avoid internal padding) - uint8_t bytes_per_sample_{2}; // Input PCM width: 2/3/4 (16/24/32-bit). 32-bit truncates to 24-bit on the wire. - uint8_t frame_in_block_{0}; // 0-191, tracks stereo frame position within block - bool preload_mode_{false}; // Whether to use preload callback vs write callback - // True when spdif_block_buf_ currently holds a complete full-silence block valid for the active - // channel status. A full silence block is deterministic for a given sample rate and word length, - // so when this is set flush_with_silence() can re-send the buffer verbatim instead of re-encoding. - bool block_buf_is_silence_block_{false}; - - // Channel Status Block (192 bits = 24 bytes, transmitted over 192 frames) - // Placed last since std::array has 1-byte alignment - std::array channel_status_{}; -}; - -} // namespace esphome::i2s_audio - -#endif // USE_I2S_AUDIO_SPDIF_MODE From 6df5f438428fa3b7b3f272264a2572fe045d7a49 Mon Sep 17 00:00:00 2001 From: Justin Bunton Date: Thu, 8 Oct 2026 16:25:49 -0400 Subject: [PATCH 3/3] Update Version --- Integrations/ESPHome/Core.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index 24442b6..8c746c7 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,5 +1,5 @@ substitutions: - version: "26.9.21.1" + version: "26.10.8.1" stable_manifest_base: "https://apolloautomation.github.io/CAST_PRO-1" beta_manifest_base: "https://github.com/ApolloAutomation/CAST_PRO-1/releases/download/beta-fw" stable_manifest_dir: "firmware"