[i2s_audio] Correct a microphone with a DC offset signal (#8751)

This commit is contained in:
Kevin Ahrendt 2025-05-12 14:30:58 -05:00 committed by GitHub
parent 11dcaf7383
commit 71e88fe9b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 85 additions and 25 deletions

View File

@ -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]))

View File

@ -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

View File

@ -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

View File

@ -10,6 +10,7 @@ microphone:
adc_type: external
pdm: false
mclk_multiple: 384
correct_dc_offset: true
on_data:
- if:
condition: