From d5dc4a39cb6f10fef73d4950fd21db3beca0f83a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:10:43 -0400 Subject: [PATCH] [i2s_audio] Fix mono sample swap and block 8-bit mono on ESP32 (#14516) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- .../components/i2s_audio/microphone/__init__.py | 6 ++++++ .../microphone/i2s_audio_microphone.cpp | 17 +++++++++-------- .../components/i2s_audio/speaker/__init__.py | 13 +++++++++---- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 16 ++++++++-------- .../speaker/media_player/audio_pipeline.cpp | 2 +- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index dd23673db5..bf583b9f81 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -46,6 +46,12 @@ def _validate_esp32_variant(config): 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: diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index cdebc214e2..eb4506071e 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -281,7 +281,7 @@ bool I2SAudioMicrophone::start_driver_() { } /* Before reading data, start the RX channel first */ - i2s_channel_enable(this->rx_handle_); + err = i2s_channel_enable(this->rx_handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "Enabling failed: %s", esp_err_to_name(err)); return false; @@ -454,13 +454,14 @@ size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_w } this->status_clear_warning(); #if defined(USE_ESP32_VARIANT_ESP32) and not defined(USE_I2S_LEGACY) - // For ESP32 8/16 bit standard mono mode samples need to be switched. - if (this->slot_mode_ == I2S_SLOT_MODE_MONO && this->slot_bit_width_ <= 16 && !this->pdm_) { - size_t samples_read = bytes_read / sizeof(int16_t); - for (int i = 0; i < samples_read; i += 2) { - int16_t tmp = buf[i]; - buf[i] = buf[i + 1]; - buf[i + 1] = tmp; + // 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 diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index 2e009a1de1..b84cf7de3b 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -100,11 +100,16 @@ def _set_stream_limits(config): def _validate_esp32_variant(config): - if config[CONF_DAC_TYPE] != "internal": - return config variant = esp32.get_esp32_variant() - if variant not in INTERNAL_DAC_VARIANTS: - raise cv.Invalid(f"{variant} does not have an internal DAC") + 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.get(CONF_BITS_PER_SAMPLE) == 8 + and config.get(CONF_CHANNEL) in (CONF_MONO, CONF_LEFT, CONF_RIGHT) + ): + raise cv.Invalid("8-bit mono mode is not supported on ESP32") return config diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index c934d12d65..a996702f8b 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -372,15 +372,15 @@ void I2SAudioSpeaker::speaker_task(void *params) { } #ifdef USE_ESP32_VARIANT_ESP32 - // For ESP32 8/16 bit mono mode samples need to be switched. + // For ESP32 16-bit mono mode, adjacent samples need to be swapped. if (this_speaker->current_stream_info_.get_channels() == 1 && - this_speaker->current_stream_info_.get_bits_per_sample() <= 16) { - size_t len = bytes_read / sizeof(int16_t); - int16_t *tmp_buf = (int16_t *) new_data; - for (size_t i = 0; i < len; i += 2) { - int16_t tmp = tmp_buf[i]; - tmp_buf[i] = tmp_buf[i + 1]; - tmp_buf[i + 1] = tmp; + this_speaker->current_stream_info_.get_bits_per_sample() == 16) { + int16_t *samples = reinterpret_cast(new_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 diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 8cea3abcfc..0822d80254 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -504,7 +504,7 @@ void AudioPipeline::decode_task(void *params) { if (!started_playback && has_stream_info) { // Verify enough data is available before starting playback std::shared_ptr temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock(); - if (temp_ring_buffer->available() >= initial_bytes_to_buffer) { + if (temp_ring_buffer != nullptr && temp_ring_buffer->available() >= initial_bytes_to_buffer) { started_playback = true; } }