diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 1fb4e9df99..7bbb94f6e3 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -30,6 +30,7 @@ DEPENDENCIES = ["i2s_audio"] CONF_ADC_PIN = "adc_pin" CONF_ADC_TYPE = "adc_type" +CONF_CORRECT_DC_OFFSET = "correct_dc_offset" CONF_PDM = "pdm" I2SAudioMicrophone = i2s_audio_ns.class_( @@ -88,10 +89,13 @@ BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( 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( { @@ -140,3 +144,5 @@ async def to_code(config): else: cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) cg.add(var.set_pdm(config[CONF_PDM])) + + cg.add(var.set_correct_dc_offset(config[CONF_CORRECT_DC_OFFSET])) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 72d1e4476c..2ff1daa197 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -12,6 +12,8 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/components/audio/audio.h" + namespace esphome { namespace i2s_audio { @@ -22,6 +24,9 @@ static const uint32_t READ_DURATION_MS = 16; static const size_t TASK_STACK_SIZE = 4096; static const ssize_t TASK_PRIORITY = 23; +// Use an exponential moving average to correct a DC offset with weight factor 1/1000 +static const int32_t DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR = 1000; + static const char *const TAG = "i2s_audio.microphone"; enum MicrophoneEventGroupBits : uint32_t { @@ -70,21 +75,11 @@ void I2SAudioMicrophone::setup() { this->mark_failed(); return; } + + this->configure_stream_settings_(); } -void I2SAudioMicrophone::start() { - if (this->is_failed()) - return; - - xSemaphoreTake(this->active_listeners_semaphore_, 0); -} - -bool I2SAudioMicrophone::start_driver_() { - if (!this->parent_->try_lock()) { - return false; // Waiting for another i2s to return lock - } - esp_err_t err; - +void I2SAudioMicrophone::configure_stream_settings_() { uint8_t channel_count = 1; #ifdef USE_I2S_LEGACY uint8_t bits_per_sample = this->bits_per_sample_; @@ -93,10 +88,10 @@ bool I2SAudioMicrophone::start_driver_() { channel_count = 2; } #else - if (this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_AUTO) { - this->slot_bit_width_ = I2S_SLOT_BIT_WIDTH_16BIT; + uint8_t bits_per_sample = 16; + if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { + bits_per_sample = this->slot_bit_width_; } - uint8_t bits_per_sample = this->slot_bit_width_; if (this->slot_mode_ == I2S_SLOT_MODE_STEREO) { channel_count = 2; @@ -114,6 +109,26 @@ bool I2SAudioMicrophone::start_driver_() { } #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); +} + +bool I2SAudioMicrophone::start_driver_() { + if (!this->parent_->try_lock()) { + return false; // Waiting for another i2s to return lock + } + esp_err_t err; + #ifdef USE_I2S_LEGACY i2s_driver_config_t config = { .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_RX), @@ -202,8 +217,6 @@ bool I2SAudioMicrophone::start_driver_() { i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config(); #if SOC_I2S_SUPPORTS_PDM_RX if (this->pdm_) { - bits_per_sample = 16; // PDM mics are always 16 bits per sample with the IDF 5 driver - i2s_pdm_rx_clk_config_t clk_cfg = { .sample_rate_hz = this->sample_rate_, .clk_src = clk_src, @@ -277,10 +290,8 @@ bool I2SAudioMicrophone::start_driver_() { } #endif - this->audio_stream_info_ = audio::AudioStreamInfo(bits_per_sample, channel_count, this->sample_rate_); - this->status_clear_error(); - + this->configure_stream_settings_(); // redetermine the settings in case some settings were changed after compilation return true; } @@ -361,9 +372,12 @@ void I2SAudioMicrophone::mic_task(void *params) { samples.resize(bytes_to_read); size_t bytes_read = this_microphone->read_(samples.data(), bytes_to_read, 2 * pdMS_TO_TICKS(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 { - delay(READ_DURATION_MS); + vTaskDelay(pdMS_TO_TICKS(READ_DURATION_MS)); } } } @@ -373,11 +387,34 @@ void I2SAudioMicrophone::mic_task(void *params) { xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STOPPED); while (true) { - // Continuously delay until the loop method delete the task - delay(10); + // Continuously delay until the loop method deletes the task + vTaskDelay(pdMS_TO_TICKS(10)); } } +void I2SAudioMicrophone::fix_dc_offset_(std::vector &data) { + 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()); + + if (total_samples == 0) { + return; + } + + int64_t offset_accumulator = 0; + for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) { + const uint32_t byte_index = sample_index * bytes_per_sample; + int32_t sample = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample); + offset_accumulator += sample; + sample -= this->dc_offset_; + audio::pack_q31_as_audio_sample(sample, &data[byte_index], bytes_per_sample); + } + + const int32_t new_offset = offset_accumulator / total_samples; + this->dc_offset_ = new_offset / DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR + + (DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR - 1) * this->dc_offset_ / + DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR; +} + size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) { size_t bytes_read = 0; #ifdef USE_I2S_LEGACY diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index 8e6d83cad3..39249e879b 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -7,8 +7,10 @@ #include "esphome/components/microphone/microphone.h" #include "esphome/core/component.h" +#include #include #include +#include namespace esphome { namespace i2s_audio { @@ -20,6 +22,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub void stop() override; void loop() override; + + void set_correct_dc_offset(bool correct_dc_offset) { this->correct_dc_offset_ = correct_dc_offset; } + #ifdef USE_I2S_LEGACY void set_din_pin(int8_t pin) { this->din_pin_ = pin; } #else @@ -41,8 +46,16 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub bool start_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, TickType_t ticks_to_wait); + /// @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}; @@ -61,6 +74,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub i2s_chan_handle_t rx_handle_; #endif bool pdm_{false}; + + bool correct_dc_offset_; + int32_t dc_offset_{0}; }; } // namespace i2s_audio diff --git a/tests/components/microphone/common.yaml b/tests/components/microphone/common.yaml index d8e4abd12a..00d33bcc3d 100644 --- a/tests/components/microphone/common.yaml +++ b/tests/components/microphone/common.yaml @@ -10,6 +10,7 @@ microphone: adc_type: external pdm: false mclk_multiple: 384 + correct_dc_offset: true on_data: - if: condition: