ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
i2s_audio_microphone.cpp
Go to the documentation of this file.
2
3#ifdef USE_ESP32
4
5#include <driver/i2s_std.h>
6#include <driver/i2s_pdm.h>
7
8#include "esphome/core/hal.h"
9#include "esphome/core/log.h"
10
12
13namespace esphome::i2s_audio {
14
15static const UBaseType_t MAX_LISTENERS = 16;
16
17static const uint32_t READ_DURATION_MS = 16;
18
19static const size_t TASK_STACK_SIZE = 4096;
20static const ssize_t TASK_PRIORITY = 23;
21
22ESPHOME_LOG_TAG(TAG, "i2s_audio.microphone");
23
25 COMMAND_STOP = (1 << 0), // stops the microphone task, set and cleared by ``loop``
26
27 TASK_STARTING = (1 << 10), // set by mic task, cleared by ``loop``
28 TASK_RUNNING = (1 << 11), // set by mic task, cleared by ``loop``
29 TASK_STOPPED = (1 << 13), // set by mic task, cleared by ``loop``
30
31 ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
32};
33
35 this->active_listeners_semaphore_ = xSemaphoreCreateCounting(MAX_LISTENERS, MAX_LISTENERS);
36 if (this->active_listeners_semaphore_ == nullptr) {
37 ESP_LOGE(TAG, "Creating semaphore failed");
38 this->mark_failed();
39 return;
40 }
41
42 this->event_group_ = xEventGroupCreate();
43 if (this->event_group_ == nullptr) {
44 ESP_LOGE(TAG, "Creating event group failed");
45 this->mark_failed();
46 return;
47 }
48
49#ifdef USE_I2S_AUDIO_FULL_DUPLEX
50 if (this->parent_->is_full_duplex() && this->parent_->is_failed()) {
51 this->mark_failed(LOG_STR("Full duplex bus setup failed"));
52 return;
53 }
54#endif
55
57}
58
60 ESP_LOGCONFIG(TAG,
61 "Microphone:\n"
62 " Pin: %d\n"
63 " PDM: %s\n"
64 " DC offset correction: %s",
65 static_cast<int8_t>(this->din_pin_), YESNO(this->pdm_), YESNO(this->correct_dc_offset_));
66}
67
69 uint8_t channel_count = 1;
70 uint8_t bits_per_sample = 16;
71 if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
72 bits_per_sample = this->slot_bit_width_;
73 }
74
75 if (this->slot_mode_ == I2S_SLOT_MODE_STEREO) {
76 channel_count = 2;
77 }
78
79#ifdef USE_ESP32_VARIANT_ESP32
80 // ESP32 reads audio aligned to a multiple of 2 bytes. For example, if configured for 24 bits per sample, then it will
81 // produce 32 bits per sample, where the actual data is in the most significant bits. Other ESP32 variants produce 24
82 // bits per sample in this situation.
83 if (bits_per_sample < 16) {
84 bits_per_sample = 16;
85 } else if ((bits_per_sample > 16) && (bits_per_sample <= 32)) {
86 bits_per_sample = 32;
87 }
88#endif
89
90 if (this->pdm_) {
91 bits_per_sample = 16; // PDM mics are always 16 bits per sample
92 }
93
94 this->audio_stream_info_ = audio::AudioStreamInfo(bits_per_sample, channel_count, this->sample_rate_);
95}
96
98 if (this->is_failed())
99 return;
100
101 xSemaphoreTake(this->active_listeners_semaphore_, 0);
102}
103
104void I2SAudioMicrophone::build_std_config_(i2s_std_config_t &std_cfg) const {
105 i2s_std_slot_config_t std_slot_cfg =
106 I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) this->slot_bit_width_, this->slot_mode_);
107 std_slot_cfg.slot_bit_width = this->slot_bit_width_;
108 std_slot_cfg.slot_mask = this->std_slot_mask_;
109
110 i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config();
111 pin_config.din = this->din_pin_;
112
113 std_cfg = {
114 .clk_cfg =
115 {
116 .sample_rate_hz = this->sample_rate_,
117 .clk_src = this->get_clock_source_(),
118 .mclk_multiple = this->mclk_multiple_,
119 },
120 .slot_cfg = std_slot_cfg,
121 .gpio_cfg = pin_config,
122 };
123}
124
125#ifdef USE_I2S_AUDIO_FULL_DUPLEX
126void I2SAudioMicrophone::build_full_duplex_config(i2s_std_config_t &std_cfg) { this->build_std_config_(std_cfg); }
127#endif
128
129bool I2SAudioMicrophone::start_driver_() {
130#ifdef USE_I2S_AUDIO_FULL_DUPLEX
131 if (this->parent_->is_full_duplex()) {
132 this->rx_handle_ = this->parent_->acquire_rx_channel();
133 if (this->rx_handle_ == nullptr) {
134 ESP_LOGE(TAG, "Full duplex channel unavailable");
135 return false;
136 }
138 return true;
139 }
140#endif
141
142 if (!this->parent_->try_lock()) {
143 return false; // Waiting for another i2s to return lock
144 }
145 this->locked_driver_ = true;
146 esp_err_t err;
147
148 i2s_chan_config_t chan_cfg = {
149 .id = this->parent_->get_port(),
150 .role = this->i2s_role_,
151 .dma_desc_num = 4,
152 .dma_frame_num = 256,
153 .auto_clear = false,
154 };
155 /* Allocate a new RX channel and get the handle of this channel */
156 err = i2s_new_channel(&chan_cfg, NULL, &this->rx_handle_);
157 if (err != ESP_OK) {
158 ESP_LOGE(TAG, "Error creating channel: %s", esp_err_to_name(err));
159 return false;
160 }
161
162#if SOC_I2S_SUPPORTS_PDM_RX
163 if (this->pdm_) {
164 i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config();
165 i2s_pdm_rx_clk_config_t clk_cfg = {
166 .sample_rate_hz = this->sample_rate_,
167 .clk_src = this->get_clock_source_(),
168 .mclk_multiple = this->mclk_multiple_,
169 .dn_sample_mode = this->pdm_dsr_,
170 };
171
172 i2s_pdm_rx_slot_config_t slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, this->slot_mode_);
173 switch (this->std_slot_mask_) {
174 case I2S_STD_SLOT_LEFT:
175 slot_cfg.slot_mask = I2S_PDM_SLOT_LEFT;
176 break;
177 case I2S_STD_SLOT_RIGHT:
178 slot_cfg.slot_mask = I2S_PDM_SLOT_RIGHT;
179 break;
180 case I2S_STD_SLOT_BOTH:
181 slot_cfg.slot_mask = I2S_PDM_SLOT_BOTH;
182 break;
183 }
184
185 /* Init the channel into PDM RX mode */
186 i2s_pdm_rx_config_t pdm_rx_cfg = {
187 .clk_cfg = clk_cfg,
188 .slot_cfg = slot_cfg,
189 .gpio_cfg =
190 {
191 .clk = pin_config.ws,
192 .din = this->din_pin_,
193 .invert_flags =
194 {
195 .clk_inv = pin_config.invert_flags.ws_inv,
196 },
197 },
198 };
199 err = i2s_channel_init_pdm_rx_mode(this->rx_handle_, &pdm_rx_cfg);
200 } else
201#endif
202 {
203 i2s_std_config_t std_cfg;
204 this->build_std_config_(std_cfg);
205 /* Initialize the channel */
206 err = i2s_channel_init_std_mode(this->rx_handle_, &std_cfg);
207 }
208 if (err != ESP_OK) {
209 ESP_LOGE(TAG, "Error initializing channel: %s", esp_err_to_name(err));
210 return false;
211 }
212
213 /* Before reading data, start the RX channel first */
214 err = i2s_channel_enable(this->rx_handle_);
215 if (err != ESP_OK) {
216 ESP_LOGE(TAG, "Enabling failed: %s", esp_err_to_name(err));
217 return false;
218 }
219
220 this->configure_stream_settings_(); // redetermine the settings in case some settings were changed after compilation
221
222 return true;
223}
224
226 if (this->state_ == microphone::STATE_STOPPED || this->is_failed())
227 return;
228
229 xSemaphoreGive(this->active_listeners_semaphore_);
230}
231
232void I2SAudioMicrophone::stop_driver_() {
233 // There is no harm continuing to unload the driver if an error is ever returned by the various functions. This
234 // ensures that we stop/unload the driver when it only partially starts.
235
236#ifdef USE_I2S_AUDIO_FULL_DUPLEX
237 if (this->parent_->is_full_duplex()) {
238 // The parent owns the channel pair, so only hand the RX channel back
239 if (this->rx_handle_ != nullptr) {
240 this->parent_->release_rx_channel();
241 this->rx_handle_ = nullptr;
242 }
243 return;
244 }
245#endif
246
247 esp_err_t err;
248 if (this->rx_handle_ != nullptr) {
249 /* Have to stop the channel before deleting it */
250 err = i2s_channel_disable(this->rx_handle_);
251 if (err != ESP_OK) {
252 ESP_LOGW(TAG, "Error stopping: %s", esp_err_to_name(err));
253 }
254 /* If the handle is not needed any more, delete it to release the channel resources */
255 err = i2s_del_channel(this->rx_handle_);
256 if (err != ESP_OK) {
257 ESP_LOGW(TAG, "Error deleting channel: %s", esp_err_to_name(err));
258 }
259 this->rx_handle_ = nullptr;
260 }
261 if (this->locked_driver_) {
262 this->parent_->unlock();
263 this->locked_driver_ = false;
264 }
265}
266
268 I2SAudioMicrophone *this_microphone = (I2SAudioMicrophone *) params;
269 xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STARTING);
270
271 { // Ensures the samples vector is freed when the task stops
272
273 const size_t bytes_to_read = this_microphone->audio_stream_info_.ms_to_bytes(READ_DURATION_MS);
274 std::vector<uint8_t> samples;
275 samples.reserve(bytes_to_read);
276
277 xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_RUNNING);
278
279 while (!(xEventGroupGetBits(this_microphone->event_group_) & MicrophoneEventGroupBits::COMMAND_STOP)) {
280 if (this_microphone->data_callbacks_.size() > 0) {
281 samples.resize(bytes_to_read);
282 size_t bytes_read = this_microphone->read_(samples.data(), bytes_to_read, 2 * READ_DURATION_MS);
283 samples.resize(bytes_read);
284 if (this_microphone->correct_dc_offset_) {
285 this_microphone->fix_dc_offset_(samples);
286 }
287 this_microphone->data_callbacks_.call(samples);
288 } else {
289 vTaskDelay(pdMS_TO_TICKS(READ_DURATION_MS));
290 }
291 }
292 }
293
294 xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STOPPED);
295 while (true) {
296 // Continuously delay until the loop method deletes the task
297 vTaskDelay(pdMS_TO_TICKS(10));
298 }
299}
300
301void I2SAudioMicrophone::fix_dc_offset_(std::vector<uint8_t> &data) {
341 const uint8_t dc_filter_shift = 10;
342 const size_t bytes_per_sample = this->audio_stream_info_.samples_to_bytes(1);
343 const uint32_t total_samples = this->audio_stream_info_.bytes_to_samples(data.size());
344 for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) {
345 const uint32_t byte_index = sample_index * bytes_per_sample;
346 int32_t input = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample);
347 int32_t output = input - this->dc_offset_prev_input_ +
348 (this->dc_offset_prev_output_ - (this->dc_offset_prev_output_ >> dc_filter_shift));
349 this->dc_offset_prev_input_ = input;
350 this->dc_offset_prev_output_ = output;
351 audio::pack_q31_as_audio_sample(output, &data[byte_index], bytes_per_sample);
352 }
353}
354
355size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, uint32_t timeout_ms) {
356 size_t bytes_read = 0;
357 esp_err_t err = i2s_channel_read(this->rx_handle_, buf, len, &bytes_read, timeout_ms);
358 if ((err != ESP_OK) && ((err != ESP_ERR_TIMEOUT) || (timeout_ms != 0))) {
359 // Ignore ESP_ERR_TIMEOUT if timeout_ms = 0, as it will read the data on the next call
360 if (!this->status_has_warning()) {
361 // Avoid spamming the logs with the error message if its repeated
362 ESP_LOGW(TAG, "Read error: %s", esp_err_to_name(err));
363 }
364 this->status_set_warning();
365 return 0;
366 }
367 if ((bytes_read == 0) && (timeout_ms > 0)) {
368 this->status_set_warning();
369 return 0;
370 }
371 this->status_clear_warning();
372#ifdef USE_ESP32_VARIANT_ESP32
373 // For ESP32 16-bit standard mono mode, adjacent samples need to be swapped.
374 if (this->slot_mode_ == I2S_SLOT_MODE_MONO && this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_16BIT && !this->pdm_) {
375 int16_t *samples = reinterpret_cast<int16_t *>(buf);
376 size_t sample_count = bytes_read / sizeof(int16_t);
377 for (size_t i = 0; i + 1 < sample_count; i += 2) {
378 int16_t tmp = samples[i];
379 samples[i] = samples[i + 1];
380 samples[i + 1] = tmp;
381 }
382 }
383#endif
384 return bytes_read;
385}
386
388 uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
389
390 if (event_group_bits & MicrophoneEventGroupBits::TASK_STARTING) {
391 ESP_LOGV(TAG, "Task started, attempting to allocate buffer");
392 xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_STARTING);
393 }
394
395 if (event_group_bits & MicrophoneEventGroupBits::TASK_RUNNING) {
396 ESP_LOGV(TAG, "Task is running and reading data");
397
398 xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_RUNNING);
400 }
401
402 if ((event_group_bits & MicrophoneEventGroupBits::TASK_STOPPED)) {
403 ESP_LOGV(TAG, "Task finished, freeing resources and uninstalling driver");
404
405 vTaskDelete(this->task_handle_);
406 this->task_handle_ = nullptr;
407 this->stop_driver_();
408 xEventGroupClearBits(this->event_group_, ALL_BITS);
409 this->status_clear_error();
410
412 }
413
414 // Start the microphone if any semaphores are taken
415 if ((uxSemaphoreGetCount(this->active_listeners_semaphore_) < MAX_LISTENERS) &&
418 }
419
420 // Stop the microphone if all semaphores are returned
421 if ((uxSemaphoreGetCount(this->active_listeners_semaphore_) == MAX_LISTENERS) &&
424 }
425
426 switch (this->state_) {
428 if (this->status_has_error()) {
429 break;
430 }
431
432 if (!this->start_driver_()) {
433 ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second");
434 this->status_momentary_error(1000);
435 this->stop_driver_(); // Stop/frees whatever possibly started
436 break;
437 }
438
439 if (this->task_handle_ == nullptr) {
440 xTaskCreate(I2SAudioMicrophone::mic_task, "mic_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
441 &this->task_handle_);
442
443 if (this->task_handle_ == nullptr) {
444 ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");
445 this->status_momentary_error(1000);
446 this->stop_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
447 }
448 }
449
450 break;
452 break;
454 xEventGroupSetBits(this->event_group_, MicrophoneEventGroupBits::COMMAND_STOP);
455 break;
457 break;
458 }
459}
460
461} // namespace esphome::i2s_audio
462
463#endif // USE_ESP32
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:274
void status_clear_error()
Definition component.h:302
bool status_has_warning() const
Definition component.h:280
bool status_has_error() const
Definition component.h:282
void status_momentary_error(uint32_t length=5000)
Set error status flag and clear it after length ms.
void status_clear_warning()
Definition component.h:296
size_t ms_to_bytes(uint32_t ms) const
Converts duration to bytes.
Definition audio.h:75
size_t samples_to_bytes(uint32_t samples) const
Converts samples to bytes.
Definition audio.h:58
uint32_t bytes_to_samples(size_t bytes) const
Convert bytes to samples.
Definition audio.h:48
i2s_std_slot_mask_t std_slot_mask_
Definition i2s_audio.h:37
i2s_slot_bit_width_t slot_bit_width_
Definition i2s_audio.h:38
i2s_clock_src_t get_clock_source_() const
Definition i2s_audio.h:26
i2s_mclk_multiple_t mclk_multiple_
Definition i2s_audio.h:41
void build_full_duplex_config(i2s_std_config_t &std_cfg) override
void configure_stream_settings_()
Starts the I2S driver.
void build_std_config_(i2s_std_config_t &std_cfg) const
Builds the standard (non-PDM) mode configuration from the configured settings.
audio::AudioStreamInfo audio_stream_info_
Definition microphone.h:45
CallbackManager< void(const std::vector< uint8_t > &)> data_callbacks_
Definition microphone.h:47
__int64 ssize_t
Definition httplib.h:178
int32_t unpack_audio_sample_to_q31(const uint8_t *data, size_t bytes_per_sample)
Unpacks a quantized audio sample into a Q31 fixed-point number.
Definition audio.h:158
void pack_q31_as_audio_sample(int32_t sample, uint8_t *data, size_t bytes_per_sample)
Packs a Q31 fixed-point number as an audio sample with the specified number of bytes per sample.
Definition audio.h:184
const char * esp_err_to_name(esp_err_t err)
ESPHOME_LOG_TAG(TAG, "i2s_audio")
const void size_t len
Definition hal.h:68
static void uint32_t