ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
micro_wake_word.cpp
Go to the documentation of this file.
1#include "micro_wake_word.h"
2
3#ifdef USE_ESP32
4
6#include "esphome/core/hal.h"
8#include "esphome/core/log.h"
9
11
12#include <algorithm>
13
14#ifdef USE_OTA
16#endif
17
19
20// Config validation checks the YAML, this checks the build: a unicore sdkconfig reached any other way
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");
23
24ESPHOME_LOG_TAG(TAG, "micro_wake_word");
25
26static const ssize_t DETECTION_QUEUE_LENGTH = 5;
27
28static const size_t DATA_TIMEOUT_MS = 50;
29
30#ifdef CONFIG_IDF_TARGET_ESP32P4
31// ESP32-P4 PIE-optimized esp-nn kernels (e.g. depthwise_conv_s8_ch1_pie) require
32// significantly more stack than other variants, causing stack protection faults at 3072.
33static const uint32_t INFERENCE_TASK_STACK_SIZE = 8192;
34#else
35static const uint32_t INFERENCE_TASK_STACK_SIZE = 3072;
36#endif
37static const UBaseType_t INFERENCE_TASK_PRIORITY = 3;
38
40 COMMAND_STOP = (1 << 0), // Signals the inference task should stop
41 COMMAND_RESET_RING_BUFFER = (1 << 1), // Signals the inference task to discard buffered audio
42 COMMAND_PAUSE_MODELS = (1 << 2), // Asks the inference task to pause at a safe point so the model lists can be
43 // mutated from the main loop
44
45 TASK_STARTING = (1 << 3),
46 TASK_RUNNING = (1 << 4),
47 TASK_STOPPING = (1 << 5),
48 TASK_STOPPED = (1 << 6),
49
50 MODELS_PAUSED = (1 << 7), // Inference task acknowledges it is paused and holds no iterators
51 COMMAND_RESUME_MODELS = (1 << 8), // Main loop signals the inference task it may resume iterating
52
53 ERROR_MEMORY = (1 << 9),
54 ERROR_INFERENCE = (1 << 10),
55
57 WARNING_MODELS_RESUME_TIMEOUT = (1 << 14), // The paused inference task gave up waiting to be released
58
60 ALL_BITS = 0xfffff, // 24 total bits available in an event group
61};
62
63// How long the main loop waits for the inference task to acknowledge a pause request before giving up.
64// The task checks for the command at the top of its loop, which runs at least every DATA_TIMEOUT_MS.
65static const uint32_t MODELS_PAUSE_TIMEOUT_MS = 500;
66// How long the paused inference task waits to be resumed before rechecking on its own. Only reached if
67// the main loop abandoned the handshake (e.g. it timed out first), so recovery just needs to be bounded.
68static const uint32_t MODELS_RESUME_TIMEOUT_MS = 1000;
69
71
72static const LogString *micro_wake_word_state_to_string(State state) {
73 switch (state) {
74 case State::STARTING:
75 return LOG_STR("STARTING");
77 return LOG_STR("DETECTING_WAKE_WORD");
78 case State::STOPPING:
79 return LOG_STR("STOPPING");
80 case State::STOPPED:
81 return LOG_STR("STOPPED");
82 default:
83 return LOG_STR("UNKNOWN");
84 }
85}
86
88 ESP_LOGCONFIG(TAG, "microWakeWord:");
89 // tskNO_AFFINITY is a cast expression, so this cannot be a preprocessor test; the compiler folds it
90 if (MICRO_WAKE_WORD_TASK_CORE == tskNO_AFFINITY) {
91 ESP_LOGCONFIG(TAG, " Inference task core: any");
92 } else {
93 ESP_LOGCONFIG(TAG, " Inference task core: %d", MICRO_WAKE_WORD_TASK_CORE);
94 }
95 ESP_LOGCONFIG(TAG, " models:");
96 for (auto &model : this->wake_word_models_) {
97 model->log_model_config();
98 }
99#ifdef USE_MICRO_WAKE_WORD_VAD
100 this->vad_model_->log_model_config();
101#endif
102}
103
105 this->frontend_config_.window.size_ms = FEATURE_DURATION_MS;
106 this->frontend_config_.window.step_size_ms = this->features_step_size_;
107 this->frontend_config_.filterbank.num_channels = PREPROCESSOR_FEATURE_SIZE;
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;
116 this->frontend_config_.pcan_gain_control.offset = PCAN_GAIN_CONTROL_OFFSET;
117 this->frontend_config_.pcan_gain_control.gain_bits = PCAN_GAIN_CONTROL_GAIN_BITS;
118 this->frontend_config_.log_scale.enable_log = LOG_SCALE_ENABLE_LOG;
119 this->frontend_config_.log_scale.scale_shift = LOG_SCALE_SCALE_SHIFT;
120
121 this->event_group_ = xEventGroupCreate();
122 if (this->event_group_ == nullptr) {
123 ESP_LOGE(TAG, "Failed to create event group");
124 this->mark_failed();
125 return;
126 }
127
128 this->detection_queue_ = xQueueCreate(DETECTION_QUEUE_LENGTH, sizeof(DetectionEvent));
129 if (this->detection_queue_ == nullptr) {
130 ESP_LOGE(TAG, "Failed to create detection event queue");
131 this->mark_failed();
132 return;
133 }
134
135 this->microphone_source_->add_data_callback([this](const std::vector<uint8_t> &data) {
136 if (this->state_ == State::STOPPED) {
137 return;
138 }
139 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
140 if (temp_ring_buffer != nullptr) {
141 // Producer-only write: never touches consumer state. If the buffer is full, ask the inference task
142 // to drain it - reset() is a consumer operation and must run on the inference task's thread.
143 // Disable partial writes so audio chunks are either fully accepted or rejected and handled below.
144 if (temp_ring_buffer->write_without_replacement(data.data(), data.size(), 0, false) == 0) {
145 xEventGroupSetBits(this->event_group_,
147 }
148 }
149 });
150
151#ifdef USE_OTA_STATE_LISTENER
153#endif
154}
155
156#ifdef USE_OTA_STATE_LISTENER
157void MicroWakeWord::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
158 if (state == ota::OTA_STARTED) {
159 this->suspend_task_();
160 } else if (state == ota::OTA_ERROR) {
161 this->resume_task_();
162 }
163}
164#endif
165
167 MicroWakeWord *this_mww = (MicroWakeWord *) params;
168
169 xEventGroupSetBits(this_mww->event_group_, EventGroupBits::TASK_STARTING);
170
171 { // Ensures any C++ objects fall out of scope to deallocate before deleting the task
172
173 const auto &stream_info = this_mww->microphone_source_->get_audio_stream_info();
174 const size_t bytes_per_frame = stream_info.frames_to_bytes(1);
175 const size_t max_fill_bytes = stream_info.ms_to_bytes(this_mww->features_step_size_);
176 std::unique_ptr<audio::RingBufferAudioSource> audio_source;
177 int8_t features_buffer[PREPROCESSOR_FEATURE_SIZE];
178
179 if (!(xEventGroupGetBits(this_mww->event_group_) & ERROR_BITS)) {
180 // Round ring buffer size down to a frame multiple so the wrap boundary never splits an int16 sample.
181 const size_t ring_buffer_size =
182 (stream_info.ms_to_bytes(this_mww->ring_buffer_duration_ms_) / bytes_per_frame) * bytes_per_frame;
183 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size);
184 if (temp_ring_buffer == nullptr) {
185 xEventGroupSetBits(this_mww->event_group_, EventGroupBits::ERROR_MEMORY);
186 } else {
187 audio_source = audio::RingBufferAudioSource::create(temp_ring_buffer, max_fill_bytes,
188 static_cast<uint8_t>(bytes_per_frame));
189 if (audio_source == nullptr) {
190 xEventGroupSetBits(this_mww->event_group_, EventGroupBits::ERROR_MEMORY);
191 } else {
192 this_mww->ring_buffer_ = temp_ring_buffer;
193 }
194 }
195 }
196
197 if (!(xEventGroupGetBits(this_mww->event_group_) & ERROR_BITS)) {
198 this_mww->microphone_source_->start();
199 xEventGroupSetBits(this_mww->event_group_, EventGroupBits::TASK_RUNNING);
200
201 while (!(xEventGroupGetBits(this_mww->event_group_) & (COMMAND_STOP | ERROR_BITS))) {
202 if (xEventGroupGetBits(this_mww->event_group_) & EventGroupBits::COMMAND_PAUSE_MODELS) {
203 // Safe point: no iterators into wake_word_models_ are held here. Acknowledge the pause and wait for the
204 // main loop to finish mutating the model lists before resuming.
205 xEventGroupSetBits(this_mww->event_group_, EventGroupBits::MODELS_PAUSED);
206 EventBits_t resume_bits = xEventGroupWaitBits(this_mww->event_group_, EventGroupBits::COMMAND_RESUME_MODELS,
207 pdTRUE, pdTRUE, pdMS_TO_TICKS(MODELS_RESUME_TIMEOUT_MS));
208 if (!(resume_bits & EventGroupBits::COMMAND_RESUME_MODELS)) {
209 // Nobody released us, so the main loop abandoned the handshake and did not mutate the lists.
210 // Rechecking the pause command below is safe, but the wait cost a second of detection, so report it.
211 xEventGroupSetBits(this_mww->event_group_, EventGroupBits::WARNING_MODELS_RESUME_TIMEOUT);
212 }
213 continue;
214 }
215
216 if (xEventGroupGetBits(this_mww->event_group_) & EventGroupBits::COMMAND_RESET_RING_BUFFER) {
217 // Producer asked us to drain; run the consumer-side reset from this thread.
218 audio_source->clear_buffered_data();
219 xEventGroupClearBits(this_mww->event_group_, EventGroupBits::COMMAND_RESET_RING_BUFFER);
220 }
221
222 audio_source->fill(pdMS_TO_TICKS(DATA_TIMEOUT_MS));
223
224 // The frontend buffers samples internally and only emits a feature once it has a full window, so we can
225 // hand it whatever the source exposes. The frontend consumes at least one sample per call, so available()
226 // strictly decreases and this loop always terminates.
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());
230
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));
235
236 if (feature_generated) {
237 if (!this_mww->update_model_probabilities_(features_buffer)) {
238 xEventGroupSetBits(this_mww->event_group_, EventGroupBits::ERROR_INFERENCE);
239 break;
240 }
241
242 // Process each model's probabilities and possibly send a Detection Event to the queue
243 this_mww->process_probabilities_();
244 }
245 }
246 }
247 }
248 }
249
250 xEventGroupSetBits(this_mww->event_group_, EventGroupBits::TASK_STOPPING);
251
252 this_mww->unload_models_();
253 this_mww->microphone_source_->stop();
254 FrontendFreeStateContents(&this_mww->frontend_state_);
255
256 xEventGroupSetBits(this_mww->event_group_, EventGroupBits::TASK_STOPPED);
257 vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
258}
259
260std::vector<WakeWordModel *> MicroWakeWord::get_wake_words() {
261 std::vector<WakeWordModel *> external_wake_word_models;
262 for (auto *model : this->wake_word_models_) {
263 if (!model->get_internal_only()) {
264 external_wake_word_models.push_back(model);
265 }
266 }
267 return external_wake_word_models;
268}
269
271
273 // When the inference task isn't running it holds no iterators into wake_word_models_, so the lists can be
274 // mutated without a handshake. The main loop is the only caller, so this state cannot change between here
275 // and the matching unlock_models_() call.
276 if (!this->inference_task_.is_created() || this->state_ == State::STOPPED) {
277 return true;
278 }
279
280 // The task is running and iterates wake_word_models_. Ask it to pause at a safe point before we mutate.
281 // Clear any stale acknowledgement from an abandoned handshake first.
282 xEventGroupClearBits(this->event_group_, EventGroupBits::MODELS_PAUSED);
283 xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_PAUSE_MODELS);
284
285 EventBits_t bits = xEventGroupWaitBits(this->event_group_, EventGroupBits::MODELS_PAUSED, pdFALSE, pdTRUE,
286 pdMS_TO_TICKS(MODELS_PAUSE_TIMEOUT_MS));
287
288 if (!(bits & EventGroupBits::MODELS_PAUSED)) {
289 // The task never acknowledged (e.g. it is busy stopping). Withdraw the request and refuse to mutate a
290 // list it might be iterating.
291 xEventGroupClearBits(this->event_group_, EventGroupBits::COMMAND_PAUSE_MODELS);
292 return false;
293 }
294 return true;
295}
296
298 if (!this->inference_task_.is_created() || this->state_ == State::STOPPED) {
299 return; // Nothing was paused
300 }
302 xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_RESUME_MODELS);
303}
304
305bool MicroWakeWord::add_runtime_model(std::unique_ptr<WakeWordModel> model) {
306 if (!model) {
307 ESP_LOGE(TAG, "Cannot add null runtime model");
308 return false;
309 }
310
311 const std::string model_id = model->get_id();
312
313 // A model without usable data can never load, so keep it out of the lists entirely. Otherwise it would be
314 // advertised to Home Assistant as selectable and the inference task would silently disable it again every
315 // time it was enabled.
316 if (!model->has_model_data()) {
317 ESP_LOGE(TAG, "Runtime model '%s' has no valid data", model_id.c_str());
318 return false;
319 }
320
321 // Reject a duplicate id against every model (compiled or runtime). The inference task only ever reads
322 // wake_word_models_, so scanning it here (on the main loop) needs no synchronization.
323 for (auto *existing : this->wake_word_models_) {
324 if (existing->get_id() == model_id) {
325 ESP_LOGW(TAG, "Wake word model '%s' already exists", model_id.c_str());
326 return false;
327 }
328 }
329
330 if (!this->try_lock_models_()) {
331 ESP_LOGE(TAG, "Timed out pausing inference task; not adding runtime model '%s'", model_id.c_str());
332 return false;
333 }
334
335 this->wake_word_models_.push_back(model.get());
336 this->runtime_models_.push_back(std::move(model));
337
338 this->unlock_models_();
339 ESP_LOGD(TAG, "Added runtime model '%s'", model_id.c_str());
340 return true;
341}
342
343bool MicroWakeWord::remove_runtime_model(const std::string &model_id) {
344 // Only runtime-downloaded models can be removed; compiled-in models never appear in runtime_models_.
345 auto runtime_it =
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; });
348 if (runtime_it == this->runtime_models_.end()) {
349 return false;
350 }
351
352 if (!this->try_lock_models_()) {
353 ESP_LOGE(TAG, "Timed out pausing inference task; not removing runtime model '%s'", model_id.c_str());
354 return false;
355 }
356
357 WakeWordModel *raw = runtime_it->get();
358 auto models_it = std::find(this->wake_word_models_.begin(), this->wake_word_models_.end(), raw);
359 if (models_it != this->wake_word_models_.end()) {
360 this->wake_word_models_.erase(models_it);
361 }
362
363 // Queued detection events hold a pointer into the model being destroyed, so drop them. The inference task
364 // is parked, so no new events can be queued concurrently. Losing an undelivered detection from another
365 // model is acceptable for this rare operation.
366 xQueueReset(this->detection_queue_);
367
368 // Free the interpreter and arenas (safe: the task is parked, not mid-inference), then destroy the model.
369 // Its ModelData releases the PSRAM model buffer once the last shared_ptr reference drops.
370 raw->unload_model();
371 this->runtime_models_.erase(runtime_it);
372
373 this->unlock_models_();
374 ESP_LOGI(TAG, "Removed runtime model '%s'", model_id.c_str());
375 return true;
376}
377
378std::vector<std::string> MicroWakeWord::get_runtime_model_ids() {
379 std::vector<std::string> ids;
380 ids.reserve(this->runtime_models_.size());
381 for (const auto &model : this->runtime_models_) {
382 ids.push_back(model->get_id());
383 }
384 return ids;
385}
386
387WakeWordModel *MicroWakeWord::get_model_by_id(const std::string &model_id) {
388 for (auto *model : this->wake_word_models_) {
389 if (model->get_id() == model_id) {
390 return model;
391 }
392 }
393 return nullptr;
394}
395
396#ifdef USE_MICRO_WAKE_WORD_VAD
397void MicroWakeWord::add_vad_model(const uint8_t *model_start, size_t model_size, uint8_t probability_cutoff,
398 size_t sliding_window_size, size_t tensor_arena_size) {
399 this->vad_model_ =
400 make_unique<VADModel>(model_start, model_size, probability_cutoff, sliding_window_size, tensor_arena_size);
401}
402#endif
403
405 if (this->inference_task_.is_created()) {
406 vTaskSuspend(this->inference_task_.get_handle());
407 }
408}
409
411 if (this->inference_task_.is_created()) {
412 vTaskResume(this->inference_task_.get_handle());
413 }
414}
415
417 uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
418
419 if (event_group_bits & EventGroupBits::ERROR_MEMORY) {
420 xEventGroupClearBits(this->event_group_, EventGroupBits::ERROR_MEMORY);
421 ESP_LOGE(TAG, "Encountered an error allocating buffers");
422 }
423
424 if (event_group_bits & EventGroupBits::ERROR_INFERENCE) {
425 xEventGroupClearBits(this->event_group_, EventGroupBits::ERROR_INFERENCE);
426 ESP_LOGE(TAG, "Encountered an error while performing an inference");
427 }
428
429 if (event_group_bits & EventGroupBits::WARNING_FULL_RING_BUFFER) {
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.");
433 }
434
435 if (event_group_bits & EventGroupBits::WARNING_MODELS_RESUME_TIMEOUT) {
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);
439 }
440
441 if (event_group_bits & EventGroupBits::TASK_STARTING) {
442 ESP_LOGD(TAG, "Inference task has started, attempting to allocate memory for buffers");
443 xEventGroupClearBits(this->event_group_, EventGroupBits::TASK_STARTING);
444 }
445
446 if (event_group_bits & EventGroupBits::TASK_RUNNING) {
447 ESP_LOGD(TAG, "Inference task is running");
448
449 xEventGroupClearBits(this->event_group_, EventGroupBits::TASK_RUNNING);
451 }
452
453 if (event_group_bits & EventGroupBits::TASK_STOPPING) {
454 ESP_LOGD(TAG, "Inference task is stopping, deallocating buffers");
455 xEventGroupClearBits(this->event_group_, EventGroupBits::TASK_STOPPING);
456 }
457
458 // Retries on a subsequent loop if the task is still running on the other core
459 if ((event_group_bits & EventGroupBits::TASK_STOPPED) && this->inference_task_.deallocate()) {
460 ESP_LOGD(TAG, "Inference task is finished, freeing task resources");
461 xEventGroupClearBits(this->event_group_, ALL_BITS);
462 xQueueReset(this->detection_queue_);
464 }
465
466 if ((this->pending_start_) && (this->state_ == State::STOPPED)) {
468 this->pending_start_ = false;
469 }
470
471 if ((this->pending_stop_) && (this->state_ == State::DETECTING_WAKE_WORD)) {
473 this->pending_stop_ = false;
474 }
475
476 switch (this->state_) {
477 case State::STARTING:
478 if (!this->inference_task_.is_created() && !this->status_has_error()) {
479 // Setup preprocesor feature generator. If done in the task, it would lock the task to its initial core, as it
480 // uses floating point operations.
481 if (!FrontendPopulateState(&this->frontend_config_, &this->frontend_state_,
483 this->status_momentary_error(1000);
484 return;
485 }
486
487 if (!this->inference_task_.create(MicroWakeWord::inference_task, "mww", INFERENCE_TASK_STACK_SIZE,
488 (void *) this, INFERENCE_TASK_PRIORITY, this->task_stack_in_psram_,
489 MICRO_WAKE_WORD_TASK_CORE)) {
490 FrontendFreeStateContents(&this->frontend_state_); // Deallocate frontend state
491 this->status_momentary_error(1000);
492 }
493 }
494 break;
496 DetectionEvent detection_event;
497 while (xQueueReceive(this->detection_queue_, &detection_event, 0)) {
498 if (detection_event.blocked_by_vad) {
499 ESP_LOGD(TAG, "Wake word model predicts '%s', but VAD model doesn't.", detection_event.wake_word->c_str());
500 } else {
501 constexpr float uint8_to_float_divisor =
502 255.0f; // Converting a quantized uint8 probability to floating point
503 ESP_LOGD(TAG, "Detected '%s' with sliding average probability is %.2f and max probability is %.2f",
504 detection_event.wake_word->c_str(), (detection_event.average_probability / uint8_to_float_divisor),
505 (detection_event.max_probability / uint8_to_float_divisor));
506 this->wake_word_detected_trigger_.trigger(*detection_event.wake_word);
507 if (this->stop_after_detection_) {
508 this->stop();
509 }
510 }
511 }
512 break;
513 }
514 case State::STOPPING:
515 xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_STOP);
516 break;
517 case State::STOPPED:
518 break;
519 }
520}
521
523 if (!this->is_ready()) {
524 ESP_LOGW(TAG, "Wake word detection can't start as the component hasn't been setup yet");
525 return;
526 }
527
528 if (this->is_failed()) {
529 ESP_LOGW(TAG, "Wake word component is marked as failed. Please check setup logs");
530 return;
531 }
532
533 if (this->is_running()) {
534 ESP_LOGW(TAG, "Wake word detection is already running");
535 return;
536 }
537
538 ESP_LOGD(TAG, "Starting wake word detection");
539
540 this->pending_start_ = true;
541 this->pending_stop_ = false;
542}
543
545 if (this->state_ == STOPPED)
546 return;
547
548 ESP_LOGD(TAG, "Stopping wake word detection");
549
550 this->pending_start_ = false;
551 this->pending_stop_ = true;
552}
553
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)));
558 this->state_ = state;
559 }
560}
561
562bool MicroWakeWord::generate_features_(const int16_t *audio_buffer, size_t samples_available,
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);
567
568 if (frontend_output.size == 0) {
569 return false;
570 }
571
572 for (size_t i = 0; i < frontend_output.size; ++i) {
573 // These scaling values are set to match the TFLite audio frontend int8 output.
574 // The feature pipeline outputs 16-bit signed integers in roughly a 0 to 670
575 // range. In training, these are then arbitrarily divided by 25.6 to get
576 // float values in the rough range of 0.0 to 26.0. This scaling is performed
577 // for historical reasons, to match up with the output of other feature
578 // generators.
579 // The process is then further complicated when we quantize the model. This
580 // means we have to scale the 0.0 to 26.0 real values to the -128 (INT8_MIN)
581 // to 127 (INT8_MAX) signed integer numbers.
582 // All this means that to get matching values from our integer feature
583 // output into the tensor input, we have to perform:
584 // input = (((feature / 25.6) / 26.0) * 256) - 128
585 // To simplify this and perform it in 32-bit integer math, we rearrange to:
586 // input = (feature * 256) / (25.6 * 26.0) - 128
587 constexpr int32_t value_scale = 256;
588 constexpr int32_t value_div = 666; // 666 = 25.6 * 26.0 after rounding
589 int32_t value = ((frontend_output.values[i] * value_scale) + (value_div / 2)) / value_div;
590
591 value += INT8_MIN; // Adds a -128; i.e., subtracts 128
592 features_buffer[i] = static_cast<int8_t>(clamp<int32_t>(value, INT8_MIN, INT8_MAX));
593 }
594
595 return true;
596}
597
599#ifdef USE_MICRO_WAKE_WORD_VAD
600 DetectionEvent vad_state = this->vad_model_->determine_detected();
601
602 this->vad_state_ = vad_state.detected; // atomic write, so thread safe
603#endif
604
605 for (auto &model : this->wake_word_models_) {
606 if (model->get_unprocessed_probability_status()) {
607 // Only detect wake words if there is a new probability since the last check
608 DetectionEvent wake_word_state = model->determine_detected();
609 if (wake_word_state.detected) {
610#ifdef USE_MICRO_WAKE_WORD_VAD
611 if (vad_state.detected) {
612#endif
613 xQueueSend(this->detection_queue_, &wake_word_state, portMAX_DELAY);
614
615 // Wake main loop immediately to process wake word detection
617
618 model->reset_probabilities();
619#ifdef USE_MICRO_WAKE_WORD_VAD
620 } else {
621 wake_word_state.blocked_by_vad = true;
622 xQueueSend(this->detection_queue_, &wake_word_state, portMAX_DELAY);
623 }
624#endif
625 }
626 }
627 }
628}
629
631 for (auto &model : this->wake_word_models_) {
632 model->unload_model();
633 }
634#ifdef USE_MICRO_WAKE_WORD_VAD
635 this->vad_model_->unload_model();
636#endif
637}
638
639bool MicroWakeWord::update_model_probabilities_(const int8_t audio_features[PREPROCESSOR_FEATURE_SIZE]) {
640 bool success = true;
641
642 for (auto &model : this->wake_word_models_) {
643 // Perform inference
644 success = success & model->perform_streaming_inference(audio_features);
645 }
646#ifdef USE_MICRO_WAKE_WORD_VAD
647 success = success & this->vad_model_->perform_streaming_inference(audio_features);
648#endif
649
650 return success;
651}
652
653} // namespace esphome::micro_wake_word
654
655#endif // USE_ESP32
uint8_t m
Definition bl0906.h:1
uint8_t raw[35]
Definition bl0939.h:0
void wake_loop_threadsafe()
Wake the main event loop from another thread or callback.
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:274
bool is_ready() const
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.
Definition static_task.h:28
TaskHandle_t get_handle() const
Get the FreeRTOS task handle.
Definition static_task.h:31
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.
Definition automation.h:556
size_t frames_to_bytes(uint32_t frames) const
Converts frames to bytes.
Definition audio.h:53
uint32_t get_sample_rate() const
Definition audio.h:30
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_
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.
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.
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 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.
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_
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()
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)
bool state
Definition fan.h:2
__int64 ssize_t
Definition httplib.h:178
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.
Definition component.h:57
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t