21static_assert(MICRO_WAKE_WORD_TASK_CORE == tskNO_AFFINITY || MICRO_WAKE_WORD_TASK_CORE < portNUM_PROCESSORS,
22 "micro_wake_word task_core is not a core of this build");
26static const ssize_t DETECTION_QUEUE_LENGTH = 5;
28static const size_t DATA_TIMEOUT_MS = 50;
30#ifdef CONFIG_IDF_TARGET_ESP32P4
33static const uint32_t INFERENCE_TASK_STACK_SIZE = 8192;
35static const uint32_t INFERENCE_TASK_STACK_SIZE = 3072;
37static const UBaseType_t INFERENCE_TASK_PRIORITY = 3;
65static const uint32_t MODELS_PAUSE_TIMEOUT_MS = 500;
68static const uint32_t MODELS_RESUME_TIMEOUT_MS = 1000;
72static const LogString *micro_wake_word_state_to_string(
State state) {
75 return LOG_STR(
"STARTING");
77 return LOG_STR(
"DETECTING_WAKE_WORD");
79 return LOG_STR(
"STOPPING");
81 return LOG_STR(
"STOPPED");
83 return LOG_STR(
"UNKNOWN");
88 ESP_LOGCONFIG(TAG,
"microWakeWord:");
90 if (MICRO_WAKE_WORD_TASK_CORE == tskNO_AFFINITY) {
91 ESP_LOGCONFIG(TAG,
" Inference task core: any");
93 ESP_LOGCONFIG(TAG,
" Inference task core: %d", MICRO_WAKE_WORD_TASK_CORE);
95 ESP_LOGCONFIG(TAG,
" models:");
97 model->log_model_config();
99#ifdef USE_MICRO_WAKE_WORD_VAD
108 this->
frontend_config_.filterbank.lower_band_limit = FILTERBANK_LOWER_BAND_LIMIT;
109 this->
frontend_config_.filterbank.upper_band_limit = FILTERBANK_UPPER_BAND_LIMIT;
110 this->
frontend_config_.noise_reduction.smoothing_bits = NOISE_REDUCTION_SMOOTHING_BITS;
111 this->
frontend_config_.noise_reduction.even_smoothing = NOISE_REDUCTION_EVEN_SMOOTHING;
112 this->
frontend_config_.noise_reduction.odd_smoothing = NOISE_REDUCTION_ODD_SMOOTHING;
113 this->
frontend_config_.noise_reduction.min_signal_remaining = NOISE_REDUCTION_MIN_SIGNAL_REMAINING;
114 this->
frontend_config_.pcan_gain_control.enable_pcan = PCAN_GAIN_CONTROL_ENABLE_PCAN;
115 this->
frontend_config_.pcan_gain_control.strength = PCAN_GAIN_CONTROL_STRENGTH;
117 this->
frontend_config_.pcan_gain_control.gain_bits = PCAN_GAIN_CONTROL_GAIN_BITS;
123 ESP_LOGE(TAG,
"Failed to create event group");
130 ESP_LOGE(TAG,
"Failed to create detection event queue");
139 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->
ring_buffer_.lock();
140 if (temp_ring_buffer !=
nullptr) {
144 if (temp_ring_buffer->write_without_replacement(data.data(), data.size(), 0,
false) == 0) {
151#ifdef USE_OTA_STATE_LISTENER
156#ifdef USE_OTA_STATE_LISTENER
176 std::unique_ptr<audio::RingBufferAudioSource> audio_source;
177 int8_t features_buffer[PREPROCESSOR_FEATURE_SIZE];
181 const size_t ring_buffer_size =
184 if (temp_ring_buffer ==
nullptr) {
188 static_cast<uint8_t
>(bytes_per_frame));
189 if (audio_source ==
nullptr) {
207 pdTRUE, pdTRUE, pdMS_TO_TICKS(MODELS_RESUME_TIMEOUT_MS));
218 audio_source->clear_buffered_data();
222 audio_source->fill(pdMS_TO_TICKS(DATA_TIMEOUT_MS));
227 while (audio_source->available() >=
sizeof(int16_t)) {
228 const size_t samples_available = audio_source->available() /
sizeof(int16_t);
229 const int16_t *audio_data =
reinterpret_cast<const int16_t *
>(audio_source->data());
231 size_t processed_samples = 0;
232 const bool feature_generated =
233 this_mww->
generate_features_(audio_data, samples_available, features_buffer, &processed_samples);
234 audio_source->consume(processed_samples *
sizeof(int16_t));
236 if (feature_generated) {
257 vTaskSuspend(
nullptr);
261 std::vector<WakeWordModel *> external_wake_word_models;
263 if (!model->get_internal_only()) {
264 external_wake_word_models.push_back(model);
267 return external_wake_word_models;
286 pdMS_TO_TICKS(MODELS_PAUSE_TIMEOUT_MS));
307 ESP_LOGE(TAG,
"Cannot add null runtime model");
311 const std::string model_id = model->get_id();
316 if (!model->has_model_data()) {
317 ESP_LOGE(TAG,
"Runtime model '%s' has no valid data", model_id.c_str());
324 if (existing->get_id() == model_id) {
325 ESP_LOGW(TAG,
"Wake word model '%s' already exists", model_id.c_str());
331 ESP_LOGE(TAG,
"Timed out pausing inference task; not adding runtime model '%s'", model_id.c_str());
335 this->wake_word_models_.push_back(model.get());
339 ESP_LOGD(TAG,
"Added runtime model '%s'", model_id.c_str());
346 std::find_if(this->
runtime_models_.begin(), this->runtime_models_.end(),
347 [&model_id](
const std::unique_ptr<WakeWordModel> &
m) { return m->get_id() == model_id; });
353 ESP_LOGE(TAG,
"Timed out pausing inference task; not removing runtime model '%s'", model_id.c_str());
358 auto models_it = std::find(this->
wake_word_models_.begin(), this->wake_word_models_.end(),
raw);
374 ESP_LOGI(TAG,
"Removed runtime model '%s'", model_id.c_str());
379 std::vector<std::string> ids;
382 ids.push_back(model->get_id());
389 if (model->get_id() == model_id) {
396#ifdef USE_MICRO_WAKE_WORD_VAD
398 size_t sliding_window_size,
size_t tensor_arena_size) {
400 make_unique<VADModel>(model_start, model_size, probability_cutoff, sliding_window_size, tensor_arena_size);
420 xEventGroupClearBits(this->
event_group_, EventGroupBits::ERROR_MEMORY);
421 ESP_LOGE(TAG,
"Encountered an error allocating buffers");
425 xEventGroupClearBits(this->
event_group_, EventGroupBits::ERROR_INFERENCE);
426 ESP_LOGE(TAG,
"Encountered an error while performing an inference");
430 xEventGroupClearBits(this->
event_group_, EventGroupBits::WARNING_FULL_RING_BUFFER);
431 ESP_LOGW(TAG,
"Not enough free bytes in ring buffer to store incoming audio data. Resetting the ring buffer. Wake "
432 "word detection accuracy will temporarily be reduced.");
436 xEventGroupClearBits(this->
event_group_, EventGroupBits::WARNING_MODELS_RESUME_TIMEOUT);
437 ESP_LOGW(TAG,
"Inference task paused for %" PRIu32
" ms without being released, so it resumed on its own",
438 MODELS_RESUME_TIMEOUT_MS);
442 ESP_LOGD(TAG,
"Inference task has started, attempting to allocate memory for buffers");
443 xEventGroupClearBits(this->
event_group_, EventGroupBits::TASK_STARTING);
447 ESP_LOGD(TAG,
"Inference task is running");
449 xEventGroupClearBits(this->
event_group_, EventGroupBits::TASK_RUNNING);
454 ESP_LOGD(TAG,
"Inference task is stopping, deallocating buffers");
455 xEventGroupClearBits(this->
event_group_, EventGroupBits::TASK_STOPPING);
460 ESP_LOGD(TAG,
"Inference task is finished, freeing task resources");
488 (
void *)
this, INFERENCE_TASK_PRIORITY, this->task_stack_in_psram_,
489 MICRO_WAKE_WORD_TASK_CORE)) {
499 ESP_LOGD(TAG,
"Wake word model predicts '%s', but VAD model doesn't.", detection_event.
wake_word->c_str());
501 constexpr float uint8_to_float_divisor =
503 ESP_LOGD(TAG,
"Detected '%s' with sliding average probability is %.2f and max probability is %.2f",
524 ESP_LOGW(TAG,
"Wake word detection can't start as the component hasn't been setup yet");
529 ESP_LOGW(TAG,
"Wake word component is marked as failed. Please check setup logs");
534 ESP_LOGW(TAG,
"Wake word detection is already running");
538 ESP_LOGD(TAG,
"Starting wake word detection");
548 ESP_LOGD(TAG,
"Stopping wake word detection");
555 if (this->
state_ != state) {
556 ESP_LOGD(TAG,
"State changed from %s to %s", LOG_STR_ARG(micro_wake_word_state_to_string(this->
state_)),
557 LOG_STR_ARG(micro_wake_word_state_to_string(
state)));
563 int8_t features_buffer[PREPROCESSOR_FEATURE_SIZE],
size_t *processed_samples) {
564 *processed_samples = 0;
565 struct FrontendOutput frontend_output =
566 FrontendProcessSamples(&this->
frontend_state_, audio_buffer, samples_available, processed_samples);
568 if (frontend_output.size == 0) {
572 for (
size_t i = 0; i < frontend_output.size; ++i) {
587 constexpr int32_t value_scale = 256;
588 constexpr int32_t value_div = 666;
589 int32_t value = ((frontend_output.values[i] * value_scale) + (value_div / 2)) / value_div;
592 features_buffer[i] =
static_cast<int8_t
>(clamp<int32_t>(value, INT8_MIN, INT8_MAX));
599#ifdef USE_MICRO_WAKE_WORD_VAD
606 if (model->get_unprocessed_probability_status()) {
610#ifdef USE_MICRO_WAKE_WORD_VAD
618 model->reset_probabilities();
619#ifdef USE_MICRO_WAKE_WORD_VAD
632 model->unload_model();
634#ifdef USE_MICRO_WAKE_WORD_VAD
644 success = success & model->perform_streaming_inference(audio_features);
646#ifdef USE_MICRO_WAKE_WORD_VAD
647 success = success & this->
vad_model_->perform_streaming_inference(audio_features);
void wake_loop_threadsafe()
Wake the main event loop from another thread or callback.
void mark_failed()
Mark this component as failed.
void status_momentary_error(uint32_t length=5000)
Set error status flag and clear it after length ms.
bool is_created() const
Check if the task has been created and not yet destroyed.
TaskHandle_t get_handle() const
Get the FreeRTOS task handle.
bool deallocate()
Delete the task (if created) and free the TCB and stack buffer.
bool create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority, bool use_psram, BaseType_t core_id=tskNO_AFFINITY)
Allocate the TCB (always internal RAM) and the stack, then create the task.
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
size_t frames_to_bytes(uint32_t frames) const
Converts frames to bytes.
uint32_t get_sample_rate() const
static std::unique_ptr< RingBufferAudioSource > create(std::shared_ptr< ring_buffer::RingBuffer > ring_buffer, size_t max_fill_bytes, uint8_t alignment_bytes=1)
Creates a new ring-buffer-backed audio source after validating its parameters.
void resume_task_()
Resumes the inference task.
microphone::MicrophoneSource * microphone_source_
QueueHandle_t detection_queue_
static void inference_task(void *params)
void process_probabilities_()
Processes any new probabilities for each model.
bool remove_runtime_model(const std::string &model_id)
Removes a runtime-downloaded wake word model and frees its interpreter, arenas, and model buffer.
EventGroupHandle_t event_group_
uint16_t ring_buffer_duration_ms_
void set_state_(State state)
WakeWordModel * get_model_by_id(const std::string &model_id)
Returns the wake word model with the given id, or nullptr if none matches (compiled or runtime).
std::vector< WakeWordModel * > wake_word_models_
void suspend_task_()
Suspends the inference task.
float get_setup_priority() const override
std::vector< std::string > get_runtime_model_ids()
Returns the ids of all runtime-downloaded models. Must be called from the main loop.
Trigger< std::string > wake_word_detected_trigger_
void add_wake_word_model(WakeWordModel *model)
bool generate_features_(const int16_t *audio_buffer, size_t samples_available, int8_t features_buffer[PREPROCESSOR_FEATURE_SIZE], size_t *processed_samples)
Generates a spectrogram feature from an input buffer of audio samples.
void dump_config() override
bool stop_after_detection_
void add_vad_model(const uint8_t *model_start, size_t model_size, uint8_t probability_cutoff, size_t sliding_window_size, size_t tensor_arena_size)
bool try_lock_models_()
Parks the inference task at a safe point (or verifies it isn't running) so the model lists may be mut...
bool add_runtime_model(std::unique_ptr< WakeWordModel > model)
Adds a runtime-downloaded wake word model.
uint8_t features_step_size_
bool update_model_probabilities_(const int8_t audio_features[PREPROCESSOR_FEATURE_SIZE])
Runs an inference with each model using the new spectrogram features.
std::vector< std::unique_ptr< WakeWordModel > > runtime_models_
struct FrontendConfig frontend_config_
std::unique_ptr< VADModel > vad_model_
std::weak_ptr< ring_buffer::RingBuffer > ring_buffer_
void unlock_models_()
Releases the inference task parked by a successful try_lock_models_() call.
void unload_models_()
Deletes each model's TFLite interpreters and frees tensor arena memory.
std::vector< WakeWordModel * > get_wake_words()
StaticTask inference_task_
struct FrontendState frontend_state_
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override
void unload_model()
Destroys the TFLite interpreter and frees the tensor and variable arenas' memory.
void add_data_callback(F &&data_callback)
audio::AudioStreamInfo get_audio_stream_info()
Gets the AudioStreamInfo of the data after processing.
void add_global_state_listener(OTAGlobalStateListener *listener)
static std::unique_ptr< RingBuffer > create(size_t len, MemoryPreference preference=MemoryPreference::EXTERNAL_FIRST)
@ WARNING_MODELS_RESUME_TIMEOUT
@ COMMAND_RESET_RING_BUFFER
@ WARNING_FULL_RING_BUFFER
ESPHOME_LOG_TAG(TAG, "micro_wake_word")
OTAGlobalCallback * get_global_ota_callback()
constexpr float AFTER_CONNECTION
For components that should be initialized after a data connection (API/MQTT) is connected.
Application App
Global storage of Application pointer - only one Application can exist.
uint8_t average_probability