mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	[i2s_audio] Correct a microphone with a DC offset signal (#8751)
This commit is contained in:
		| @@ -30,6 +30,7 @@ DEPENDENCIES = ["i2s_audio"] | |||||||
|  |  | ||||||
| CONF_ADC_PIN = "adc_pin" | CONF_ADC_PIN = "adc_pin" | ||||||
| CONF_ADC_TYPE = "adc_type" | CONF_ADC_TYPE = "adc_type" | ||||||
|  | CONF_CORRECT_DC_OFFSET = "correct_dc_offset" | ||||||
| CONF_PDM = "pdm" | CONF_PDM = "pdm" | ||||||
|  |  | ||||||
| I2SAudioMicrophone = i2s_audio_ns.class_( | I2SAudioMicrophone = i2s_audio_ns.class_( | ||||||
| @@ -88,10 +89,13 @@ BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( | |||||||
|         default_sample_rate=16000, |         default_sample_rate=16000, | ||||||
|         default_channel=CONF_RIGHT, |         default_channel=CONF_RIGHT, | ||||||
|         default_bits_per_sample="32bit", |         default_bits_per_sample="32bit", | ||||||
|  |     ).extend( | ||||||
|  |         { | ||||||
|  |             cv.Optional(CONF_CORRECT_DC_OFFSET, default=False): cv.boolean, | ||||||
|  |         } | ||||||
|     ) |     ) | ||||||
| ).extend(cv.COMPONENT_SCHEMA) | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = cv.All( | ||||||
|     cv.typed_schema( |     cv.typed_schema( | ||||||
|         { |         { | ||||||
| @@ -140,3 +144,5 @@ async def to_code(config): | |||||||
|     else: |     else: | ||||||
|         cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) |         cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) | ||||||
|         cg.add(var.set_pdm(config[CONF_PDM])) |         cg.add(var.set_pdm(config[CONF_PDM])) | ||||||
|  |  | ||||||
|  |     cg.add(var.set_correct_dc_offset(config[CONF_CORRECT_DC_OFFSET])) | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ | |||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | #include "esphome/components/audio/audio.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace i2s_audio { | 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 size_t TASK_STACK_SIZE = 4096; | ||||||
| static const ssize_t TASK_PRIORITY = 23; | 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"; | static const char *const TAG = "i2s_audio.microphone"; | ||||||
|  |  | ||||||
| enum MicrophoneEventGroupBits : uint32_t { | enum MicrophoneEventGroupBits : uint32_t { | ||||||
| @@ -70,21 +75,11 @@ void I2SAudioMicrophone::setup() { | |||||||
|     this->mark_failed(); |     this->mark_failed(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   this->configure_stream_settings_(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void I2SAudioMicrophone::start() { | void I2SAudioMicrophone::configure_stream_settings_() { | ||||||
|   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; |  | ||||||
|  |  | ||||||
|   uint8_t channel_count = 1; |   uint8_t channel_count = 1; | ||||||
| #ifdef USE_I2S_LEGACY | #ifdef USE_I2S_LEGACY | ||||||
|   uint8_t bits_per_sample = this->bits_per_sample_; |   uint8_t bits_per_sample = this->bits_per_sample_; | ||||||
| @@ -93,10 +88,10 @@ bool I2SAudioMicrophone::start_driver_() { | |||||||
|     channel_count = 2; |     channel_count = 2; | ||||||
|   } |   } | ||||||
| #else | #else | ||||||
|   if (this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_AUTO) { |   uint8_t bits_per_sample = 16; | ||||||
|     this->slot_bit_width_ = I2S_SLOT_BIT_WIDTH_16BIT; |   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) { |   if (this->slot_mode_ == I2S_SLOT_MODE_STEREO) { | ||||||
|     channel_count = 2; |     channel_count = 2; | ||||||
| @@ -114,6 +109,26 @@ bool I2SAudioMicrophone::start_driver_() { | |||||||
|   } |   } | ||||||
| #endif | #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 | #ifdef USE_I2S_LEGACY | ||||||
|   i2s_driver_config_t config = { |   i2s_driver_config_t config = { | ||||||
|       .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_RX), |       .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(); |   i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config(); | ||||||
| #if SOC_I2S_SUPPORTS_PDM_RX | #if SOC_I2S_SUPPORTS_PDM_RX | ||||||
|   if (this->pdm_) { |   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 = { |     i2s_pdm_rx_clk_config_t clk_cfg = { | ||||||
|         .sample_rate_hz = this->sample_rate_, |         .sample_rate_hz = this->sample_rate_, | ||||||
|         .clk_src = clk_src, |         .clk_src = clk_src, | ||||||
| @@ -277,10 +290,8 @@ bool I2SAudioMicrophone::start_driver_() { | |||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   this->audio_stream_info_ = audio::AudioStreamInfo(bits_per_sample, channel_count, this->sample_rate_); |  | ||||||
|  |  | ||||||
|   this->status_clear_error(); |   this->status_clear_error(); | ||||||
|  |   this->configure_stream_settings_();  // redetermine the settings in case some settings were changed after compilation | ||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -361,9 +372,12 @@ void I2SAudioMicrophone::mic_task(void *params) { | |||||||
|         samples.resize(bytes_to_read); |         samples.resize(bytes_to_read); | ||||||
|         size_t bytes_read = this_microphone->read_(samples.data(), bytes_to_read, 2 * pdMS_TO_TICKS(READ_DURATION_MS)); |         size_t bytes_read = this_microphone->read_(samples.data(), bytes_to_read, 2 * pdMS_TO_TICKS(READ_DURATION_MS)); | ||||||
|         samples.resize(bytes_read); |         samples.resize(bytes_read); | ||||||
|  |         if (this_microphone->correct_dc_offset_) { | ||||||
|  |           this_microphone->fix_dc_offset_(samples); | ||||||
|  |         } | ||||||
|         this_microphone->data_callbacks_.call(samples); |         this_microphone->data_callbacks_.call(samples); | ||||||
|       } else { |       } 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); |   xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STOPPED); | ||||||
|   while (true) { |   while (true) { | ||||||
|     // Continuously delay until the loop method delete the task |     // Continuously delay until the loop method deletes the task | ||||||
|     delay(10); |     vTaskDelay(pdMS_TO_TICKS(10)); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void I2SAudioMicrophone::fix_dc_offset_(std::vector<uint8_t> &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 I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) { | ||||||
|   size_t bytes_read = 0; |   size_t bytes_read = 0; | ||||||
| #ifdef USE_I2S_LEGACY | #ifdef USE_I2S_LEGACY | ||||||
|   | |||||||
| @@ -7,8 +7,10 @@ | |||||||
| #include "esphome/components/microphone/microphone.h" | #include "esphome/components/microphone/microphone.h" | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
|  |  | ||||||
|  | #include <freertos/FreeRTOS.h> | ||||||
| #include <freertos/event_groups.h> | #include <freertos/event_groups.h> | ||||||
| #include <freertos/semphr.h> | #include <freertos/semphr.h> | ||||||
|  | #include <freertos/task.h> | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace i2s_audio { | namespace i2s_audio { | ||||||
| @@ -20,6 +22,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub | |||||||
|   void stop() override; |   void stop() override; | ||||||
|  |  | ||||||
|   void loop() override; |   void loop() override; | ||||||
|  |  | ||||||
|  |   void set_correct_dc_offset(bool correct_dc_offset) { this->correct_dc_offset_ = correct_dc_offset; } | ||||||
|  |  | ||||||
| #ifdef USE_I2S_LEGACY | #ifdef USE_I2S_LEGACY | ||||||
|   void set_din_pin(int8_t pin) { this->din_pin_ = pin; } |   void set_din_pin(int8_t pin) { this->din_pin_ = pin; } | ||||||
| #else | #else | ||||||
| @@ -41,8 +46,16 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub | |||||||
|   bool start_driver_(); |   bool start_driver_(); | ||||||
|   void stop_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<uint8_t> &data); | ||||||
|  |  | ||||||
|   size_t read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait); |   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); |   static void mic_task(void *params); | ||||||
|  |  | ||||||
|   SemaphoreHandle_t active_listeners_semaphore_{nullptr}; |   SemaphoreHandle_t active_listeners_semaphore_{nullptr}; | ||||||
| @@ -61,6 +74,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub | |||||||
|   i2s_chan_handle_t rx_handle_; |   i2s_chan_handle_t rx_handle_; | ||||||
| #endif | #endif | ||||||
|   bool pdm_{false}; |   bool pdm_{false}; | ||||||
|  |  | ||||||
|  |   bool correct_dc_offset_; | ||||||
|  |   int32_t dc_offset_{0}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace i2s_audio | }  // namespace i2s_audio | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ microphone: | |||||||
|     adc_type: external |     adc_type: external | ||||||
|     pdm: false |     pdm: false | ||||||
|     mclk_multiple: 384 |     mclk_multiple: 384 | ||||||
|  |     correct_dc_offset: true | ||||||
|     on_data: |     on_data: | ||||||
|       - if: |       - if: | ||||||
|           condition: |           condition: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user