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_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])) | ||||
|   | ||||
| @@ -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<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 bytes_read = 0; | ||||
| #ifdef USE_I2S_LEGACY | ||||
|   | ||||
| @@ -7,8 +7,10 @@ | ||||
| #include "esphome/components/microphone/microphone.h" | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| #include <freertos/FreeRTOS.h> | ||||
| #include <freertos/event_groups.h> | ||||
| #include <freertos/semphr.h> | ||||
| #include <freertos/task.h> | ||||
|  | ||||
| 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<uint8_t> &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 | ||||
|   | ||||
| @@ -10,6 +10,7 @@ microphone: | ||||
|     adc_type: external | ||||
|     pdm: false | ||||
|     mclk_multiple: 384 | ||||
|     correct_dc_offset: true | ||||
|     on_data: | ||||
|       - if: | ||||
|           condition: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user