#include "i2s_audio_microphone.h" #ifdef USE_ESP32 #include #include #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/components/audio/audio.h" namespace esphome { namespace i2s_audio { static const UBaseType_t MAX_LISTENERS = 16; static const uint32_t READ_DURATION_MS = 16; static const size_t TASK_STACK_SIZE = 4096; static const ssize_t TASK_PRIORITY = 23; static const char *const TAG = "i2s_audio.microphone"; enum MicrophoneEventGroupBits : uint32_t { COMMAND_STOP = (1 << 0), // stops the microphone task, set and cleared by ``loop`` TASK_STARTING = (1 << 10), // set by mic task, cleared by ``loop`` TASK_RUNNING = (1 << 11), // set by mic task, cleared by ``loop`` TASK_STOPPED = (1 << 13), // set by mic task, cleared by ``loop`` ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits }; void I2SAudioMicrophone::setup() { this->active_listeners_semaphore_ = xSemaphoreCreateCounting(MAX_LISTENERS, MAX_LISTENERS); if (this->active_listeners_semaphore_ == nullptr) { ESP_LOGE(TAG, "Creating semaphore failed"); this->mark_failed(); return; } this->event_group_ = xEventGroupCreate(); if (this->event_group_ == nullptr) { ESP_LOGE(TAG, "Creating event group failed"); this->mark_failed(); return; } this->configure_stream_settings_(); } void I2SAudioMicrophone::dump_config() { ESP_LOGCONFIG(TAG, "Microphone:\n" " Pin: %d\n" " PDM: %s\n" " DC offset correction: %s", static_cast(this->din_pin_), YESNO(this->pdm_), YESNO(this->correct_dc_offset_)); } void I2SAudioMicrophone::configure_stream_settings_() { uint8_t channel_count = 1; uint8_t bits_per_sample = 16; if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { bits_per_sample = this->slot_bit_width_; } if (this->slot_mode_ == I2S_SLOT_MODE_STEREO) { channel_count = 2; } #ifdef USE_ESP32_VARIANT_ESP32 // ESP32 reads audio aligned to a multiple of 2 bytes. For example, if configured for 24 bits per sample, then it will // produce 32 bits per sample, where the actual data is in the most significant bits. Other ESP32 variants produce 24 // bits per sample in this situation. if (bits_per_sample < 16) { bits_per_sample = 16; } else if ((bits_per_sample > 16) && (bits_per_sample <= 32)) { bits_per_sample = 32; } #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 } this->locked_driver_ = true; esp_err_t err; i2s_chan_config_t chan_cfg = { .id = this->parent_->get_port(), .role = this->i2s_role_, .dma_desc_num = 4, .dma_frame_num = 256, .auto_clear = false, }; /* Allocate a new RX channel and get the handle of this channel */ err = i2s_new_channel(&chan_cfg, NULL, &this->rx_handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "Error creating channel: %s", esp_err_to_name(err)); return false; } i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT; #ifdef I2S_CLK_SRC_APLL if (this->use_apll_) { clk_src = I2S_CLK_SRC_APLL; } #endif i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config(); #if SOC_I2S_SUPPORTS_PDM_RX if (this->pdm_) { i2s_pdm_rx_clk_config_t clk_cfg = { .sample_rate_hz = this->sample_rate_, .clk_src = clk_src, .mclk_multiple = this->mclk_multiple_, .dn_sample_mode = I2S_PDM_DSR_8S, }; i2s_pdm_rx_slot_config_t slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, this->slot_mode_); switch (this->std_slot_mask_) { case I2S_STD_SLOT_LEFT: slot_cfg.slot_mask = I2S_PDM_SLOT_LEFT; break; case I2S_STD_SLOT_RIGHT: slot_cfg.slot_mask = I2S_PDM_SLOT_RIGHT; break; case I2S_STD_SLOT_BOTH: slot_cfg.slot_mask = I2S_PDM_SLOT_BOTH; break; } /* Init the channel into PDM RX mode */ i2s_pdm_rx_config_t pdm_rx_cfg = { .clk_cfg = clk_cfg, .slot_cfg = slot_cfg, .gpio_cfg = { .clk = pin_config.ws, .din = this->din_pin_, .invert_flags = { .clk_inv = pin_config.invert_flags.ws_inv, }, }, }; err = i2s_channel_init_pdm_rx_mode(this->rx_handle_, &pdm_rx_cfg); } else #endif { i2s_std_clk_config_t clk_cfg = { .sample_rate_hz = this->sample_rate_, .clk_src = clk_src, .mclk_multiple = this->mclk_multiple_, }; i2s_std_slot_config_t std_slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) this->slot_bit_width_, this->slot_mode_); std_slot_cfg.slot_bit_width = this->slot_bit_width_; std_slot_cfg.slot_mask = this->std_slot_mask_; pin_config.din = this->din_pin_; i2s_std_config_t std_cfg = { .clk_cfg = clk_cfg, .slot_cfg = std_slot_cfg, .gpio_cfg = pin_config, }; /* Initialize the channel */ err = i2s_channel_init_std_mode(this->rx_handle_, &std_cfg); } if (err != ESP_OK) { ESP_LOGE(TAG, "Error initializing channel: %s", esp_err_to_name(err)); return false; } /* Before reading data, start the RX channel first */ err = i2s_channel_enable(this->rx_handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "Enabling failed: %s", esp_err_to_name(err)); return false; } this->configure_stream_settings_(); // redetermine the settings in case some settings were changed after compilation return true; } void I2SAudioMicrophone::stop() { if (this->state_ == microphone::STATE_STOPPED || this->is_failed()) return; xSemaphoreGive(this->active_listeners_semaphore_); } void I2SAudioMicrophone::stop_driver_() { // There is no harm continuing to unload the driver if an error is ever returned by the various functions. This // ensures that we stop/unload the driver when it only partially starts. esp_err_t err; if (this->rx_handle_ != nullptr) { /* Have to stop the channel before deleting it */ err = i2s_channel_disable(this->rx_handle_); if (err != ESP_OK) { ESP_LOGW(TAG, "Error stopping: %s", esp_err_to_name(err)); } /* If the handle is not needed any more, delete it to release the channel resources */ err = i2s_del_channel(this->rx_handle_); if (err != ESP_OK) { ESP_LOGW(TAG, "Error deleting channel: %s", esp_err_to_name(err)); } this->rx_handle_ = nullptr; } if (this->locked_driver_) { this->parent_->unlock(); this->locked_driver_ = false; } } void I2SAudioMicrophone::mic_task(void *params) { I2SAudioMicrophone *this_microphone = (I2SAudioMicrophone *) params; xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STARTING); { // Ensures the samples vector is freed when the task stops const size_t bytes_to_read = this_microphone->audio_stream_info_.ms_to_bytes(READ_DURATION_MS); std::vector samples; samples.reserve(bytes_to_read); xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_RUNNING); while (!(xEventGroupGetBits(this_microphone->event_group_) & MicrophoneEventGroupBits::COMMAND_STOP)) { if (this_microphone->data_callbacks_.size() > 0) { 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 { vTaskDelay(pdMS_TO_TICKS(READ_DURATION_MS)); } } } xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STOPPED); while (true) { // Continuously delay until the loop method deletes the task vTaskDelay(pdMS_TO_TICKS(10)); } } void I2SAudioMicrophone::fix_dc_offset_(std::vector &data) { /** * From https://www.musicdsp.org/en/latest/Filters/135-dc-filter.html: * * y(n) = x(n) - x(n-1) + R * y(n-1) * R = 1 - (pi * 2 * frequency / samplerate) * * From https://en.wikipedia.org/wiki/Hearing_range: * The human range is commonly given as 20Hz up. * * From https://en.wikipedia.org/wiki/High-resolution_audio: * A reasonable upper bound for sample rate seems to be 96kHz. * * Calculate R value for 20Hz on a 96kHz sample rate: * R = 1 - (pi * 2 * 20 / 96000) * R = 0.9986910031 * * Transform floating point to bit-shifting approximation: * output = input - prev_input + R * prev_output * output = input - prev_input + (prev_output - (prev_output >> S)) * * Approximate bit-shift value S from R: * R = 1 - (1 >> S) * R = 1 - (1 / 2^S) * R = 1 - 2^-S * 0.9986910031 = 1 - 2^-S * S = 9.57732 ~= 10 * * Actual R from S: * R = 1 - 2^-10 = 0.9990234375 * * Confirm this has effect outside human hearing on 96000kHz sample: * 0.9990234375 = 1 - (pi * 2 * f / 96000) * f = 14.9208Hz * * Confirm this has effect outside human hearing on PDM 16kHz sample: * 0.9990234375 = 1 - (pi * 2 * f / 16000) * f = 2.4868Hz * */ const uint8_t dc_filter_shift = 10; 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()); for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) { const uint32_t byte_index = sample_index * bytes_per_sample; int32_t input = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample); int32_t output = input - this->dc_offset_prev_input_ + (this->dc_offset_prev_output_ - (this->dc_offset_prev_output_ >> dc_filter_shift)); this->dc_offset_prev_input_ = input; this->dc_offset_prev_output_ = output; audio::pack_q31_as_audio_sample(output, &data[byte_index], bytes_per_sample); } } size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) { size_t bytes_read = 0; // i2s_channel_read expects the timeout value in ms, not ticks esp_err_t err = i2s_channel_read(this->rx_handle_, buf, len, &bytes_read, pdTICKS_TO_MS(ticks_to_wait)); if ((err != ESP_OK) && ((err != ESP_ERR_TIMEOUT) || (ticks_to_wait != 0))) { // Ignore ESP_ERR_TIMEOUT if ticks_to_wait = 0, as it will read the data on the next call if (!this->status_has_warning()) { // Avoid spamming the logs with the error message if its repeated ESP_LOGW(TAG, "Read error: %s", esp_err_to_name(err)); } this->status_set_warning(); return 0; } if ((bytes_read == 0) && (ticks_to_wait > 0)) { this->status_set_warning(); return 0; } this->status_clear_warning(); #ifdef USE_ESP32_VARIANT_ESP32 // For ESP32 16-bit standard mono mode, adjacent samples need to be swapped. if (this->slot_mode_ == I2S_SLOT_MODE_MONO && this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_16BIT && !this->pdm_) { int16_t *samples = reinterpret_cast(buf); size_t sample_count = bytes_read / sizeof(int16_t); for (size_t i = 0; i + 1 < sample_count; i += 2) { int16_t tmp = samples[i]; samples[i] = samples[i + 1]; samples[i + 1] = tmp; } } #endif return bytes_read; } void I2SAudioMicrophone::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); if (event_group_bits & MicrophoneEventGroupBits::TASK_STARTING) { ESP_LOGV(TAG, "Task started, attempting to allocate buffer"); xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_STARTING); } if (event_group_bits & MicrophoneEventGroupBits::TASK_RUNNING) { ESP_LOGV(TAG, "Task is running and reading data"); xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_RUNNING); this->state_ = microphone::STATE_RUNNING; } if ((event_group_bits & MicrophoneEventGroupBits::TASK_STOPPED)) { ESP_LOGV(TAG, "Task finished, freeing resources and uninstalling driver"); vTaskDelete(this->task_handle_); this->task_handle_ = nullptr; this->stop_driver_(); xEventGroupClearBits(this->event_group_, ALL_BITS); this->status_clear_error(); this->state_ = microphone::STATE_STOPPED; } // Start the microphone if any semaphores are taken if ((uxSemaphoreGetCount(this->active_listeners_semaphore_) < MAX_LISTENERS) && (this->state_ == microphone::STATE_STOPPED)) { this->state_ = microphone::STATE_STARTING; } // Stop the microphone if all semaphores are returned if ((uxSemaphoreGetCount(this->active_listeners_semaphore_) == MAX_LISTENERS) && (this->state_ == microphone::STATE_RUNNING)) { this->state_ = microphone::STATE_STOPPING; } switch (this->state_) { case microphone::STATE_STARTING: if (this->status_has_error()) { break; } if (!this->start_driver_()) { ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); this->status_momentary_error("driver_fail", 1000); this->stop_driver_(); // Stop/frees whatever possibly started break; } if (this->task_handle_ == nullptr) { xTaskCreate(I2SAudioMicrophone::mic_task, "mic_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, &this->task_handle_); if (this->task_handle_ == nullptr) { ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); this->status_momentary_error("task_fail", 1000); this->stop_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt } } break; case microphone::STATE_RUNNING: break; case microphone::STATE_STOPPING: xEventGroupSetBits(this->event_group_, MicrophoneEventGroupBits::COMMAND_STOP); break; case microphone::STATE_STOPPED: break; } } } // namespace i2s_audio } // namespace esphome #endif // USE_ESP32