ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
voice_assistant.cpp
Go to the documentation of this file.
1#include "voice_assistant.h"
3
4#ifdef USE_VOICE_ASSISTANT
5
8#include "esphome/core/log.h"
9
10#include <cinttypes>
11#include <cstdio>
12
14
15ESPHOME_LOG_TAG(TAG, "voice_assistant");
16
17static constexpr uint32_t RESET_CONVERSATION_TIMEOUT_ID = 0;
18static constexpr uint32_t PLAYBACK_TIMEOUT_ID = 1;
19static constexpr uint32_t SPEAKER_IDLE_TIMEOUT_ID = 2;
20static constexpr uint32_t TIMER_TICK_INTERVAL_ID = 3;
21
22#ifdef SAMPLE_RATE_HZ
23#undef SAMPLE_RATE_HZ
24#endif
25
26static const size_t SAMPLE_RATE_HZ = 16000;
27
28static const size_t RING_BUFFER_SAMPLES = 512 * SAMPLE_RATE_HZ / 1000; // 512 ms * 16 kHz/ 1000 ms
29static const size_t RING_BUFFER_SIZE = RING_BUFFER_SAMPLES * sizeof(int16_t);
30static const size_t SEND_BUFFER_SAMPLES = 32 * SAMPLE_RATE_HZ / 1000; // 32ms * 16kHz / 1000ms
31static const size_t SEND_BUFFER_SIZE = SEND_BUFFER_SAMPLES * sizeof(int16_t);
32static const size_t RECEIVE_SIZE = 1024;
33static const size_t SPEAKER_BUFFER_SIZE = 16 * RECEIVE_SIZE;
34
35// If one microphone channel keeps producing audio while another configured channel produces none for this
36// long, treat the silent channel as failed and stop the stream. A working microphone exposes a chunk every
37// SEND_BUFFER_SAMPLES (32 ms), so this is far longer than any legitimate gap between chunks.
38static const uint32_t AUDIO_CHANNEL_STALL_TIMEOUT_MS = 2000;
39
41
43 this->mic_source_->add_data_callback([this](const std::vector<uint8_t> &data) {
44 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
45 if (temp_ring_buffer != nullptr) {
46 temp_ring_buffer->write((void *) data.data(), data.size());
47 }
48 });
49
50 // Second microphone channel
51 if (this->mic_source2_ != nullptr) {
52 this->mic_source2_->add_data_callback([this](const std::vector<uint8_t> &data) {
53 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer2_.lock();
54 if (temp_ring_buffer != nullptr) {
55 temp_ring_buffer->write((void *) data.data(), data.size());
56 }
57 });
58 }
59
60#ifdef USE_MEDIA_PLAYER
61 if (this->media_player_ != nullptr) {
63 switch (state) {
66 // State changed to announcing after receiving the url
68 }
69 break;
70 default:
72 // No longer announcing the TTS response
74 }
75 break;
76 }
77 });
78 }
79#endif
80}
81
83
85#if USE_NETWORK_IPV4
86 this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
87#else
88 this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP);
89#endif
90 if (this->socket_ == nullptr) {
91 ESP_LOGE(TAG, "Could not create socket");
92 this->mark_failed();
93 return false;
94 }
95 int enable = 1;
96 int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
97 if (err != 0) {
98 ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
99 // we can still continue
100 }
101 err = this->socket_->setblocking(false);
102 if (err != 0) {
103 ESP_LOGE(TAG, "Socket unable to set nonblocking mode: errno %d", err);
104 this->mark_failed();
105 return false;
106 }
107
108#ifdef USE_SPEAKER
109 if (this->speaker_ != nullptr) {
110 struct sockaddr_storage server;
111
112 socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), 6055);
113 if (sl == 0) {
114 ESP_LOGE(TAG, "Socket unable to set sockaddr: errno %d", errno);
115 this->mark_failed();
116 return false;
117 }
118
119 err = this->socket_->bind((struct sockaddr *) &server, sizeof(server));
120 if (err != 0) {
121 ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
122 this->mark_failed();
123 return false;
124 }
125 }
126#endif
127 this->udp_socket_running_ = true;
128 return true;
129}
130
132#ifdef USE_SPEAKER
133 if ((this->speaker_ != nullptr) && (this->speaker_buffer_ == nullptr)) {
134 RAMAllocator<uint8_t> speaker_allocator;
135 this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE);
136 if (this->speaker_buffer_ == nullptr) {
137 ESP_LOGW(TAG, "Could not allocate speaker buffer");
138 return false;
139 }
140 }
141#endif
142
143 if (this->audio_source_ == nullptr) {
144 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(RING_BUFFER_SIZE);
145 if (temp_ring_buffer == nullptr) {
146 ESP_LOGE(TAG, "Could not allocate ring buffer");
147 return false;
148 }
149 // Zero-copy source that reads directly from the ring buffer; frame-aligned to never split an int16 sample.
150 this->audio_source_ = audio::RingBufferAudioSource::create(temp_ring_buffer, SEND_BUFFER_SIZE, sizeof(int16_t));
151 if (this->audio_source_ == nullptr) {
152 ESP_LOGE(TAG, "Could not allocate audio source");
153 return false;
154 }
155 this->ring_buffer_ = temp_ring_buffer;
156 }
157
158 // Second microphone channel
159 if ((this->mic_source2_ != nullptr) && (this->audio_source2_ == nullptr)) {
160 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(RING_BUFFER_SIZE);
161 if (temp_ring_buffer == nullptr) {
162 ESP_LOGE(TAG, "Could not allocate second ring buffer");
163 return false;
164 }
165 this->audio_source2_ = audio::RingBufferAudioSource::create(temp_ring_buffer, SEND_BUFFER_SIZE, sizeof(int16_t));
166 if (this->audio_source2_ == nullptr) {
167 ESP_LOGE(TAG, "Could not allocate second audio source");
168 return false;
169 }
170 this->ring_buffer2_ = temp_ring_buffer;
171 }
172
173 return true;
174}
175
177 if (this->audio_source_ != nullptr) {
178 this->audio_source_->clear_buffered_data();
179 }
180
181 // Second microphone channel
182 if (this->audio_source2_ != nullptr) {
183 this->audio_source2_->clear_buffered_data();
184 }
185
186 // Reset the multi-channel stall watchdog (see audio_channel_stall_start_).
188
189#ifdef USE_SPEAKER
190 if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) {
191 memset(this->speaker_buffer_, 0, SPEAKER_BUFFER_SIZE);
192
193 this->speaker_buffer_size_ = 0;
194 this->speaker_buffer_index_ = 0;
195 this->speaker_bytes_received_ = 0;
196 }
197#endif
198}
199
201 // Destroying each source releases its ring buffer; the matching weak_ptr then expires automatically.
202 this->audio_source_.reset();
203
204 // Second microphone channel
205 this->audio_source2_.reset();
206
207#ifdef USE_SPEAKER
208 if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) {
209 RAMAllocator<uint8_t> speaker_deallocator;
210 speaker_deallocator.deallocate(this->speaker_buffer_, SPEAKER_BUFFER_SIZE);
211 this->speaker_buffer_ = nullptr;
212 }
213#endif
214}
215
217 this->conversation_id_ = "";
218 ESP_LOGD(TAG, "reset conversation ID");
219}
220
222 // Both microphone channels are sent together, if configured. Home Assistant feeds one of the
223 // channels to its speech-to-text stream and treats an empty payload on that channel as
224 // end-of-stream, and the device cannot know which channel it picked, so only send once every
225 // configured channel has audio exposed, and always send them together. We don't target any
226 // particular message size: Home Assistant re-chunks the audio, and each fill() exposes at most
227 // SEND_BUFFER_SIZE bytes.
228 while (true) {
229 // fill() exposes a new chunk, or returns 0 if a previous chunk is still exposed; available()
230 // reports the currently exposed bytes either way.
231 this->audio_source_->fill(0);
232 size_t available = this->audio_source_->available();
233 size_t available2 = 0;
234 if (this->audio_source2_ != nullptr) {
235 this->audio_source2_->fill(0);
236 available2 = this->audio_source2_->available();
237 }
238
239 const bool channel_empty = (available == 0);
240 const bool channel2_empty = (this->audio_source2_ != nullptr) && (available2 == 0);
241 if (channel_empty || channel2_empty) {
242 // A configured channel has no audio yet, so keep any chunk exposed on the other channel for the
243 // next pass rather than sending an empty payload.
244 this->handle_channel_stall_(available, available2);
245 break;
246 }
247
248 // Both channels have audio exposed; clear any in-progress stall timer.
250
252 // Zero-copy: send_message() copies the data out before we consume it.
253 msg.data = this->audio_source_->data();
254 msg.data_len = available;
255 if (this->audio_source2_ != nullptr) {
256 msg.data2 = this->audio_source2_->data();
257 msg.data2_len = available2;
258 }
259
260 if (!this->api_client_->send_message(msg)) {
261 // Keep the chunk exposed and retry next pass, the same shape as
262 // APIConnection::try_send_camera_image_(): the slice is only lost if
263 // the ring buffer overflows before the TCP buffer clears, instead of
264 // on every refusal. The api layer already reports the refusal at V.
265 return;
266 }
267
268 this->audio_source_->consume(available);
269 if (this->audio_source2_ != nullptr) {
270 this->audio_source2_->consume(available2);
271 }
272 }
273}
274
275void VoiceAssistant::handle_channel_stall_(size_t available, size_t available2) {
276 // Called when at least one configured channel has no audio exposed. When one channel has data and the
277 // other does not, watch how long the empty channel stays starved: Home Assistant has no stream timeout
278 // and would never tell us to stop, so a channel that fails outright would otherwise hang streaming
279 // forever with the live channel's chunk held. Stop the stream with an error after a prolonged imbalance.
280 if ((available == 0) && (available2 == 0)) {
281 // Both channels are idle (no audio buffered yet); normal, not a stalled channel.
283 return;
284 }
285
287 if (this->audio_channel_stall_start_ == 0) {
288 this->audio_channel_stall_start_ = now;
289 } else if ((now - this->audio_channel_stall_start_) >= AUDIO_CHANNEL_STALL_TIMEOUT_MS) {
290 ESP_LOGW(TAG, "Mic channel %d stalled, stopping stream", (available == 0) ? 0 : 1);
292 this->signal_stop_();
294 this->defer([this]() {
295 this->error_trigger_.trigger("mic-channel-stalled", "A microphone channel stopped producing audio");
296 });
297 }
298}
299
301 if (this->api_client_ == nullptr && this->state_ != State::IDLE && this->state_ != State::STOP_MICROPHONE &&
303 if (this->mic_source_->is_running() || (this->mic_source2_ && this->mic_source2_->is_running()) ||
304 this->state_ == State::STARTING_MICROPHONE) {
306 } else {
308 }
309 this->continuous_ = false;
310 this->signal_stop_();
311 this->clear_buffers_();
312 return;
313 }
314 switch (this->state_) {
315 case State::IDLE: {
316 if (this->continuous_ && this->desired_state_ == State::IDLE) {
317 this->idle_trigger_.trigger();
319 } else {
320 this->deallocate_buffers_();
321 }
322 break;
323 }
325 ESP_LOGD(TAG, "Starting Microphone");
326 if (!this->allocate_buffers_()) {
327 this->status_set_error(LOG_STR("Failed to allocate buffers"));
328 return;
329 }
330 if (this->status_has_error()) {
331 this->status_clear_error();
332 }
333 this->clear_buffers_();
334
335 this->mic_source_->start();
336 if (this->mic_source2_) {
337 this->mic_source2_->start();
338 }
340 break;
341 }
343 if (this->mic_source_->is_running() && (!this->mic_source2_ || this->mic_source2_->is_running())) {
344 this->set_state_(this->desired_state_);
345 }
346 break;
347 }
349 ESP_LOGD(TAG, "Requesting start");
350 uint32_t flags = 0;
351 if (!this->continue_conversation_ && this->use_wake_word_)
353 if (this->silence_detection_)
357 audio_settings.auto_gain = this->auto_gain_;
358 audio_settings.volume_multiplier = this->volume_multiplier_;
359
361 msg.start = true;
363 msg.flags = flags;
364 msg.audio_settings = std::move(audio_settings);
366
367 // Reset media player state tracking
368#ifdef USE_MEDIA_PLAYER
369 if (this->media_player_ != nullptr) {
371 }
372#endif
373
374 if (this->api_client_ == nullptr || !this->api_client_->send_message(msg)) {
375 ESP_LOGW(TAG, "Could not request start");
376 this->error_trigger_.trigger("not-connected", "Could not request start");
377 this->continuous_ = false;
379 break;
380 }
382 this->set_timeout(RESET_CONVERSATION_TIMEOUT_ID, this->conversation_timeout_,
383 [this]() { this->reset_conversation_id(); });
384 break;
385 }
387 break; // State changed when udp server port received
388 }
390 if (this->audio_mode_ == AUDIO_MODE_API) {
391 this->stream_api_audio_();
392 } else {
393 // UDP (will eventually be deprecated)
394 // Only the primary microphone channel is used
395 while (true) {
396 this->audio_source_->fill(0);
397 size_t available = this->audio_source_->available();
398 if (available == 0) {
399 break;
400 }
401 if (!this->udp_socket_running_) {
402 if (!this->start_udp_socket_()) {
404 break;
405 }
406 }
407 this->socket_->sendto(this->audio_source_->data(), available, 0, (struct sockaddr *) &this->dest_addr_,
408 sizeof(this->dest_addr_));
409 this->audio_source_->consume(available);
410 }
411 } // audio mode
412 break;
413 }
415 // Check both microphone channels
416 bool is_running = this->mic_source_->is_running();
417 bool is_running2 = false;
418 if (this->mic_source2_) {
419 is_running2 = this->mic_source2_->is_running();
420 }
421 if (is_running || is_running2) {
422 if (is_running) {
423 this->mic_source_->stop();
424 }
425 if (is_running2) {
426 this->mic_source2_->stop();
427 }
429 } else {
430 this->set_state_(this->desired_state_);
431 }
432 break;
433 }
435 // Check both microphone channels
436 bool is_stopped = this->mic_source_->is_stopped();
437 bool is_stopped2 = true;
438 if (this->mic_source2_) {
439 is_stopped2 = this->mic_source2_->is_stopped();
440 }
441 if (is_stopped && is_stopped2) {
442 this->set_state_(this->desired_state_);
443 }
444 break;
445 }
447 break; // State changed by events
448 }
450 bool playing = false;
451#ifdef USE_SPEAKER
452 if (this->speaker_ != nullptr) {
453 ssize_t received_len = 0;
454 if (this->audio_mode_ == AUDIO_MODE_UDP) {
455 if (this->speaker_buffer_index_ + RECEIVE_SIZE < SPEAKER_BUFFER_SIZE) {
456 received_len = this->socket_->read(this->speaker_buffer_ + this->speaker_buffer_index_, RECEIVE_SIZE);
457 if (received_len > 0) {
458 this->speaker_buffer_index_ += received_len;
459 this->speaker_buffer_size_ += received_len;
460 this->speaker_bytes_received_ += received_len;
461 }
462 } else {
463 ESP_LOGD(TAG, "Receive buffer full");
464 }
465 }
466 // Build a small buffer of audio before sending to the speaker
467 bool end_of_stream = this->stream_ended_ && (this->audio_mode_ == AUDIO_MODE_API || received_len < 0);
468 if (this->speaker_bytes_received_ > RECEIVE_SIZE * 4 || end_of_stream)
469 this->write_speaker_();
470 if (this->wait_for_stream_end_) {
471 this->cancel_timeout(PLAYBACK_TIMEOUT_ID);
472 if (end_of_stream) {
473 ESP_LOGD(TAG, "End of audio stream received");
474 this->cancel_timeout(SPEAKER_IDLE_TIMEOUT_ID);
476 }
477 break; // We dont want to timeout here as the STREAM_END event will take care of that.
478 }
479 playing = this->speaker_->is_running();
480 }
481#endif
482#ifdef USE_MEDIA_PLAYER
483 if (this->media_player_ != nullptr) {
485
488 this->cancel_timeout(PLAYBACK_TIMEOUT_ID);
489 ESP_LOGD(TAG, "Announcement finished playing");
491
493 msg.success = true;
494 if (!this->api_client_->send_message(msg)) {
495 API_LOG_MSG_DROPPED(TAG, "Announce-finished");
496 }
497 break;
498 }
499 }
500#endif
501 if (playing) {
503 }
504 break;
505 }
507#ifdef USE_SPEAKER
508 if (this->speaker_ != nullptr) {
509 if (this->speaker_buffer_size_ > 0) {
510 this->write_speaker_();
511 break;
512 }
513 if (this->speaker_->has_buffered_data() || this->speaker_->is_running()) {
514 break;
515 }
516 ESP_LOGD(TAG, "Speaker has finished outputting all audio");
517 this->speaker_->stop();
518 this->cancel_timeout(SPEAKER_IDLE_TIMEOUT_ID);
519 this->cancel_timeout(PLAYBACK_TIMEOUT_ID);
520
521 this->clear_buffers_();
522
523 this->wait_for_stream_end_ = false;
524 this->stream_ended_ = false;
525
527 }
528#endif
529 if (this->continue_conversation_) {
531 } else {
533 }
534 break;
535 }
536 default:
537 break;
538 }
539}
540
541#ifdef USE_SPEAKER
543 if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) {
544 if (this->speaker_buffer_size_ > 0) {
545 size_t write_chunk = std::min<size_t>(this->speaker_buffer_size_, 4 * 1024);
546 size_t written = this->speaker_->play(this->speaker_buffer_, write_chunk);
547 if (written > 0) {
548 memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written);
551 this->set_timeout(SPEAKER_IDLE_TIMEOUT_ID, 5000, [this]() { this->speaker_->stop(); });
552 } else {
553 ESP_LOGV(TAG, "Speaker buffer full, trying again next loop");
554 }
555 }
556 }
557}
558#endif
559
561 if (!subscribe) {
562 if (this->api_client_ == nullptr || client != this->api_client_) {
563 ESP_LOGE(TAG, "Client attempting to unsubscribe that is not the current API Client");
564 return;
565 }
566 this->api_client_ = nullptr;
568 return;
569 }
570
571 if (this->api_client_ != nullptr) {
572 char current_peername[socket::SOCKADDR_STR_LEN];
573 char new_peername[socket::SOCKADDR_STR_LEN];
574 ESP_LOGE(TAG,
575 "Multiple API Clients attempting to connect to Voice Assistant\n"
576 " Current client: %s (%s)\n"
577 " New client: %s (%s)",
578 this->api_client_->get_name(), this->api_client_->get_peername_to(current_peername), client->get_name(),
579 client->get_peername_to(new_peername));
580 return;
581 }
582
583 this->api_client_ = client;
585}
586
587static const LogString *voice_assistant_state_to_string(State state) {
588 switch (state) {
589 case State::IDLE:
590 return LOG_STR("IDLE");
592 return LOG_STR("START_MICROPHONE");
594 return LOG_STR("STARTING_MICROPHONE");
596 return LOG_STR("WAIT_FOR_VAD");
598 return LOG_STR("WAITING_FOR_VAD");
600 return LOG_STR("START_PIPELINE");
602 return LOG_STR("STARTING_PIPELINE");
604 return LOG_STR("STREAMING_MICROPHONE");
606 return LOG_STR("STOP_MICROPHONE");
608 return LOG_STR("STOPPING_MICROPHONE");
610 return LOG_STR("AWAITING_RESPONSE");
612 return LOG_STR("STREAMING_RESPONSE");
614 return LOG_STR("RESPONSE_FINISHED");
615 default:
616 return LOG_STR("UNKNOWN");
617 }
618};
619
621 State old_state = this->state_;
622 this->state_ = state;
623 ESP_LOGD(TAG, "State changed from %s to %s", LOG_STR_ARG(voice_assistant_state_to_string(old_state)),
624 LOG_STR_ARG(voice_assistant_state_to_string(state)));
625}
626
628 this->set_state_(state);
629 this->desired_state_ = desired_state;
630 ESP_LOGD(TAG, "Desired state set to %s", LOG_STR_ARG(voice_assistant_state_to_string(desired_state)));
631}
632
635 // loop() moves to the desired state once the microphone has stopped
636 this->desired_state_ = state;
637 ESP_LOGD(TAG, "Desired state set to %s", LOG_STR_ARG(voice_assistant_state_to_string(state)));
638 } else if (state != this->state_) {
639 this->set_state_(state, state);
640 }
641}
642
644 ESP_LOGE(TAG, "Failed to start server. See Home Assistant logs for more details.");
645 this->error_trigger_.trigger("failed-to-start", "Failed to start server. See Home Assistant logs for more details.");
647}
648
650 if (this->state_ != State::STARTING_PIPELINE) {
651 this->signal_stop_();
652 return;
653 }
654
655 ESP_LOGD(TAG, "Client started, streaming microphone");
657
658 // Both microphone channels
659 if (this->mic_source_->is_running() && (!this->mic_source2_ || this->mic_source2_->is_running())) {
661 } else {
663 }
664}
665
666void VoiceAssistant::start_streaming(struct sockaddr_storage *addr, uint16_t port) {
667 if (this->state_ != State::STARTING_PIPELINE) {
668 this->signal_stop_();
669 return;
670 }
671
672 ESP_LOGD(TAG, "Client started, streaming microphone");
674
675 if (this->mic_source2_ != nullptr) {
676 ESP_LOGW(TAG, "UDP audio mode does not support a second microphone channel; only the primary will be streamed");
677 }
678
679 memcpy(&this->dest_addr_, addr, sizeof(this->dest_addr_));
680 switch (this->dest_addr_.ss_family) {
681#if USE_NETWORK_IPV4
682 case AF_INET:
683 ((struct sockaddr_in *) &this->dest_addr_)->sin_port = htons(port);
684 break;
685#endif
686#if LWIP_IPV6
687 case AF_INET6:
688 ((struct sockaddr_in6 *) &this->dest_addr_)->sin6_port = htons(port);
689 break;
690#endif
691 default:
692 ESP_LOGW(TAG, "Unknown address family: %d", this->dest_addr_.ss_family);
693 return;
694 }
695
696 // Only primary microphone channel over UDP
697 if (this->mic_source_->is_running()) {
699 } else {
701 }
702}
703
704void VoiceAssistant::request_start(bool continuous, bool silence_detection) {
705 if (this->api_client_ == nullptr) {
706 ESP_LOGE(TAG, "No API client connected");
708 this->continuous_ = false;
709 return;
710 }
711 if (this->state_ == State::IDLE) {
712 this->continuous_ = continuous;
713 this->silence_detection_ = silence_detection;
714
716 }
717}
718
720 this->continuous_ = false;
721 this->continue_conversation_ = false;
722
723 switch (this->state_) {
724 case State::IDLE:
725 break;
732 break;
735 this->signal_stop_();
737 break;
740 // A response queued behind the microphone shutdown is cancelled like a playing one and keeps its
741 // state, so it finishes through the same path (playback timeout, stream end) once the microphone
742 // has stopped instead of leaving those behind for the next request
744 this->cancel_response_();
745 } else {
747 }
748 break;
750 this->signal_stop_();
751 break;
753 this->cancel_response_();
754 break;
756 break; // Let the incoming audio stream finish then it will go to idle.
757 }
758}
759
761#ifdef USE_MEDIA_PLAYER
762 // Stop any ongoing media player announcement
763 if (this->media_player_ != nullptr) {
764 this->media_player_->make_call()
766 .set_announcement(true)
767 .perform();
768 }
769 if (this->started_streaming_tts_) {
770 // Haven't reached the TTS_END stage, so send the stop signal to HA.
771 this->signal_stop_();
772 }
773#endif
774}
775
777 memset(&this->dest_addr_, 0, sizeof(this->dest_addr_));
778 if (this->api_client_ == nullptr) {
779 return;
780 }
781 ESP_LOGD(TAG, "Signaling stop");
783 msg.start = false;
784 if (!this->api_client_->send_message(msg)) {
785 API_LOG_MSG_DROPPED(TAG, "Stop request");
786 }
787}
788
790 this->set_timeout(PLAYBACK_TIMEOUT_ID, this->tts_playback_start_timeout_, [this]() {
791 this->cancel_timeout(SPEAKER_IDLE_TIMEOUT_ID);
793
794 if (this->api_client_ == nullptr)
795 return;
797 msg.success = true;
798 if (!this->api_client_->send_message(msg)) {
799 API_LOG_MSG_DROPPED(TAG, "Announce-finished");
800 }
801 });
802}
803
805 ESP_LOGD(TAG, "Event Type: %" PRId32, msg.event_type);
806 switch (msg.event_type) {
808 ESP_LOGD(TAG, "Assist Pipeline running");
809#ifdef USE_MEDIA_PLAYER
810 this->started_streaming_tts_ = false;
811 for (const auto &arg : msg.data) {
812 if (arg.name == "url") {
813 this->tts_response_url_ = arg.value;
814 }
815 }
816#endif
817 this->defer([this]() { this->start_trigger_.trigger(); });
818 break;
820 break;
822 ESP_LOGD(TAG, "Wake word detected");
823 this->defer([this]() { this->wake_word_detected_trigger_.trigger(); });
824 break;
825 }
827 ESP_LOGD(TAG, "STT started");
828 this->defer([this]() { this->listening_trigger_.trigger(); });
829 break;
831 std::string text;
832 for (const auto &arg : msg.data) {
833 if (arg.name == "text") {
834 text = arg.value;
835 }
836 }
837 if (text.empty()) {
838 ESP_LOGW(TAG, "No text in STT_END event");
839 return;
840 } else if (text.length() > 500) {
841 text.resize(497);
842 text += "...";
843 }
844 ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str());
845 this->defer([this, text]() { this->stt_end_trigger_.trigger(text); });
846 break;
847 }
849 ESP_LOGD(TAG, "Intent started");
850 this->defer([this]() { this->intent_start_trigger_.trigger(); });
851 break;
853 ESP_LOGD(TAG, "Intent progress");
854 std::string tts_url_for_trigger;
855#ifdef USE_MEDIA_PLAYER
856 if (this->media_player_ != nullptr) {
857 for (const auto &arg : msg.data) {
858 if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) {
860
862
863 this->started_streaming_tts_ = true;
865
866 tts_url_for_trigger = this->tts_response_url_;
867 this->tts_response_url_.clear(); // Reset streaming URL
869 }
870 }
871 }
872#endif
873 this->defer([this, tts_url_for_trigger]() { this->intent_progress_trigger_.trigger(tts_url_for_trigger); });
874 break;
875 }
877 for (const auto &arg : msg.data) {
878 if (arg.name == "conversation_id") {
879 this->conversation_id_ = arg.value;
880 } else if (arg.name == "continue_conversation") {
881 this->continue_conversation_ = (arg.value == "1");
882 }
883 }
884 this->defer([this]() { this->intent_end_trigger_.trigger(); });
885 break;
886 }
888 std::string text;
889 for (const auto &arg : msg.data) {
890 if (arg.name == "text") {
891 text = arg.value;
892 }
893 }
894 if (text.empty()) {
895 ESP_LOGW(TAG, "No text in TTS_START event");
896 return;
897 }
898 if (text.length() > 500) {
899 text.resize(497);
900 text += "...";
901 }
902 ESP_LOGD(TAG, "Response: \"%s\"", text.c_str());
903 this->defer([this, text]() {
904 this->tts_start_trigger_.trigger(text);
905#ifdef USE_SPEAKER
906 if (this->speaker_ != nullptr) {
907 this->speaker_->start();
908 }
909#endif
910 });
911 break;
912 }
914 std::string url;
915 for (const auto &arg : msg.data) {
916 if (arg.name == "url") {
917 url = arg.value;
918 }
919 }
920 if (url.empty()) {
921 ESP_LOGW(TAG, "No url in TTS_END event");
922 return;
923 }
924 ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str());
925 this->defer([this, url]() {
926#ifdef USE_MEDIA_PLAYER
927 if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) {
929
931
933 }
934 this->started_streaming_tts_ = false; // Helps indicate reaching the TTS_END stage
935#endif
936 this->tts_end_trigger_.trigger(url);
937 });
938 // The intent progress stage may already have moved to streaming response
940 break;
941 }
943 ESP_LOGD(TAG, "Assist Pipeline ended");
944 if ((this->state_ == State::START_PIPELINE) || (this->state_ == State::STARTING_PIPELINE) ||
946 // Microphone is running, stop it
948 } else if (this->state_ == State::AWAITING_RESPONSE) {
949 // No TTS start event ("nevermind")
951 } else if (this->desired_state_ == State::AWAITING_RESPONSE) {
952 // No TTS start event while the microphone is still stopping
954 }
955 this->defer([this]() { this->end_trigger_.trigger(); });
956 break;
957 }
959 std::string code;
960 std::string message;
961 for (const auto &arg : msg.data) {
962 if (arg.name == "code") {
963 code = arg.value;
964 } else if (arg.name == "message") {
965 message = arg.value;
966 }
967 }
968 if (code == "wake-word-timeout" || code == "wake_word_detection_aborted" || code == "no_wake_word") {
969 // Don't change state here since either the "tts-end" or "run-end" events will do it.
970 return;
971 } else if (code == "wake-provider-missing" || code == "wake-engine-missing") {
972 // Wake word is not set up or not ready on Home Assistant so stop and do not retry until user starts again.
973 this->defer([this, code, message]() {
974 this->request_stop();
975 this->error_trigger_.trigger(code, message);
976 });
977 return;
978 }
979 ESP_LOGE(TAG, "Error: %s - %s", code.c_str(), message.c_str());
980 if (this->state_ != State::IDLE) {
981 this->signal_stop_();
983 }
984 this->defer([this, code, message]() { this->error_trigger_.trigger(code, message); });
985 break;
986 }
988#ifdef USE_SPEAKER
989 if (this->speaker_ != nullptr) {
990 this->wait_for_stream_end_ = true;
991 ESP_LOGD(TAG, "TTS stream start");
992 this->defer([this] { this->tts_stream_start_trigger_.trigger(); });
993 }
994#endif
995 break;
996 }
998#ifdef USE_SPEAKER
999 if (this->speaker_ != nullptr) {
1000 this->stream_ended_ = true;
1001 ESP_LOGD(TAG, "TTS stream end");
1002 }
1003#endif
1004 break;
1005 }
1007 ESP_LOGD(TAG, "Starting STT by VAD");
1008 this->defer([this]() { this->stt_vad_start_trigger_.trigger(); });
1009 break;
1011 ESP_LOGD(TAG, "STT by VAD end");
1013 this->defer([this]() { this->stt_vad_end_trigger_.trigger(); });
1014 break;
1015 default:
1016 ESP_LOGD(TAG, "Unhandled event type: %" PRId32, msg.event_type);
1017 break;
1018 }
1019}
1020
1022#ifdef USE_SPEAKER // We should never get to this function if there is no speaker anyway
1023 if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) {
1024 if (this->speaker_buffer_index_ + msg.data_len <= SPEAKER_BUFFER_SIZE) {
1025 memcpy(this->speaker_buffer_ + this->speaker_buffer_index_, msg.data, msg.data_len);
1026 this->speaker_buffer_index_ += msg.data_len;
1027 this->speaker_buffer_size_ += msg.data_len;
1028 this->speaker_bytes_received_ += msg.data_len;
1029 this->write_speaker_();
1030 ESP_LOGV(TAG, "Received audio: %u bytes from API", msg.data_len);
1031 } else {
1032 ESP_LOGE(TAG, "Cannot receive audio, buffer is full");
1033 }
1034 }
1035#endif
1036}
1037
1039 // Find existing timer or add a new one
1040 auto it = this->timers_.begin();
1041 for (; it != this->timers_.end(); ++it) {
1042 if (it->id == msg.timer_id)
1043 break;
1044 }
1045 if (it == this->timers_.end()) {
1046 this->timers_.push_back({});
1047 it = this->timers_.end() - 1;
1048 }
1049 it->id = msg.timer_id;
1050 it->name = msg.name;
1051 it->total_seconds = msg.total_seconds;
1052 it->seconds_left = msg.seconds_left;
1053 it->is_active = msg.is_active;
1054
1055 char timer_buf[Timer::TO_STR_BUFFER_SIZE];
1056 ESP_LOGD(TAG,
1057 "Timer Event\n"
1058 " Type: %" PRId32 "\n"
1059 " %s",
1060 msg.event_type, it->to_str(timer_buf));
1061
1062 switch (msg.event_type) {
1064 this->timer_started_trigger_.trigger(*it);
1065 break;
1067 this->timer_updated_trigger_.trigger(*it);
1068 break;
1070 this->timer_cancelled_trigger_.trigger(*it);
1071 this->timers_.erase(it);
1072 break;
1074 this->timer_finished_trigger_.trigger(*it);
1075 this->timers_.erase(it);
1076 break;
1077 }
1078
1079 if (this->timers_.empty()) {
1080 this->cancel_interval(TIMER_TICK_INTERVAL_ID);
1081 this->timer_tick_running_ = false;
1082 } else if (!this->timer_tick_running_) {
1083 this->set_interval(TIMER_TICK_INTERVAL_ID, 1000, [this]() { this->timer_tick_(); });
1084 this->timer_tick_running_ = true;
1085 }
1086}
1087
1089 for (auto &timer : this->timers_) {
1090 if (timer.is_active && timer.seconds_left > 0) {
1091 timer.seconds_left--;
1092 }
1093 }
1094 this->timer_tick_trigger_.trigger(this->timers_);
1095}
1096
1098#ifdef USE_MEDIA_PLAYER
1099 if (this->media_player_ != nullptr) {
1100 this->tts_start_trigger_.trigger(msg.text);
1101
1103
1104 if (!msg.preannounce_media_id.empty()) {
1106 }
1107 // Enqueueing a URL with an empty playlist will still play the file immediately
1108 this->media_player_->make_call()
1111 .set_announcement(true)
1112 .perform();
1114
1116
1117 if (this->continuous_) {
1119 } else {
1121 }
1122
1124 this->end_trigger_.trigger();
1125 }
1126#endif
1127}
1128
1129void VoiceAssistant::on_set_configuration(const std::vector<std::string> &active_wake_words) {
1130#ifdef USE_MICRO_WAKE_WORD
1131 if (this->micro_wake_word_) {
1132 // Disable all wake words first
1133 for (auto &model : this->micro_wake_word_->get_wake_words()) {
1134 model->disable();
1135 }
1136
1137 // Enable only active wake words
1138 for (const auto &ww_id : active_wake_words) {
1139 for (auto &model : this->micro_wake_word_->get_wake_words()) {
1140 if (model->get_id() == ww_id) {
1141 model->enable();
1142 ESP_LOGD(TAG, "Enabled wake word: %s (id=%s)", model->get_wake_word().c_str(), model->get_id().c_str());
1143 }
1144 }
1145 }
1146 }
1147#endif
1148};
1149
1151 this->config_.available_wake_words.clear();
1152 this->config_.active_wake_words.clear();
1153
1154#ifdef USE_MICRO_WAKE_WORD
1155 if (this->micro_wake_word_) {
1157
1158 for (auto &model : this->micro_wake_word_->get_wake_words()) {
1159 if (model->is_enabled()) {
1160 this->config_.active_wake_words.push_back(model->get_id());
1161 }
1162
1163 WakeWord wake_word;
1164 wake_word.id = model->get_id();
1165 wake_word.wake_word = model->get_wake_word();
1166 for (const auto &lang : model->get_trained_languages()) {
1167 wake_word.trained_languages.push_back(lang);
1168 }
1169 this->config_.available_wake_words.push_back(std::move(wake_word));
1170 }
1171 } else {
1172#endif
1173 // No microWakeWord
1175#ifdef USE_MICRO_WAKE_WORD
1176 }
1177#endif
1178
1179 return this->config_;
1180};
1181
1182VoiceAssistant *global_voice_assistant = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
1183
1184} // namespace esphome::voice_assistant
1185
1186#endif // USE_VOICE_ASSISTANT
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void mark_failed()
Mark this component as failed.
bool cancel_interval(const char *name)
Cancel an interval function.
Definition component.cpp:92
void status_clear_error()
Definition component.h:302
bool cancel_timeout(const char *name)
Cancel a timeout function.
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
void defer(const char *name, std::function< void()> &&f)
Defer a callback to the next loop() call with a const char* name.
bool status_has_error() const
Definition component.h:282
void set_interval(const char *name, uint32_t interval, std::function< void()> &&f)
Set an interval function with a const char* name.
Definition component.cpp:88
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2298
void deallocate(T *p, size_t n)
Definition helpers.h:2355
T * allocate(size_t n)
Definition helpers.h:2325
StringRef is a reference to a string owned by something else.
Definition string_ref.h:31
constexpr bool empty() const
Definition string_ref.h:81
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
Definition automation.h:556
const char * get_peername_to(std::span< char, socket::SOCKADDR_STR_LEN > buf) const
Get peer name (IP address) into caller-provided buffer, returns buf for convenience.
const char * get_name() const
bool send_message(const T &msg)
Returns false as soon as the TCP buffer is full.
enums::VoiceAssistantEvent event_type
Definition api_pb2.h:3017
std::vector< VoiceAssistantEventData > data
Definition api_pb2.h:3018
VoiceAssistantAudioSettings audio_settings
Definition api_pb2.h:2963
enums::VoiceAssistantTimerEvent event_type
Definition api_pb2.h:3064
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.
MediaPlayerCall & set_media_url(const std::string &url)
MediaPlayerCall & set_announcement(bool announce)
MediaPlayerCall & set_command(MediaPlayerCommand command)
void add_on_state_callback(F &&callback)
std::vector< WakeWordModel * > get_wake_words()
void add_data_callback(F &&data_callback)
static std::unique_ptr< RingBuffer > create(size_t len, MemoryPreference preference=MemoryPreference::EXTERNAL_FIRST)
virtual size_t play(const uint8_t *data, size_t length)=0
Plays the provided audio data.
bool is_running() const
Definition speaker.h:68
virtual bool has_buffered_data() const =0
virtual void start()=0
virtual void stop()=0
std::unique_ptr< socket::Socket > socket_
microphone::MicrophoneSource * mic_source2_
void on_timer_event(const api::VoiceAssistantTimerEventResponse &msg)
void cancel_response_()
Stop a playing or queued response and tell Home Assistant when the TTS stream is still open.
void on_audio(const api::VoiceAssistantAudio &msg)
std::unique_ptr< audio::RingBufferAudioSource > audio_source_
std::weak_ptr< ring_buffer::RingBuffer > ring_buffer2_
std::weak_ptr< ring_buffer::RingBuffer > ring_buffer_
media_player::MediaPlayer * media_player_
void client_subscription(api::APIConnection *client, bool subscribe)
MediaPlayerResponseState media_player_response_state_
void on_event(const api::VoiceAssistantEventResponse &msg)
Trigger< std::string, std::string > error_trigger_
void set_state_after_mic_stop_(State state)
Move to state, or only make it the desired state while the microphone is still stopping.
Trigger< const std::vector< Timer > & > timer_tick_trigger_
Trigger< std::string > intent_progress_trigger_
void on_announce(const api::VoiceAssistantAnnounceRequest &msg)
void request_start(bool continuous, bool silence_detection)
void handle_channel_stall_(size_t available, size_t available2)
std::unique_ptr< audio::RingBufferAudioSource > audio_source2_
microphone::MicrophoneSource * mic_source_
micro_wake_word::MicroWakeWord * micro_wake_word_
void on_set_configuration(const std::vector< std::string > &active_wake_words)
const LogString * message
Definition component.cpp:35
uint16_t flags
bool state
Definition fan.h:2
uint32_t socklen_t
Definition headers.h:109
__int64 ssize_t
Definition httplib.h:178
@ VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD
Definition api_pb2.h:269
@ VOICE_ASSISTANT_REQUEST_USE_VAD
Definition api_pb2.h:268
@ VOICE_ASSISTANT_TIMER_UPDATED
Definition api_pb2.h:292
@ VOICE_ASSISTANT_TIMER_STARTED
Definition api_pb2.h:291
@ VOICE_ASSISTANT_TIMER_FINISHED
Definition api_pb2.h:294
@ VOICE_ASSISTANT_TIMER_CANCELLED
Definition api_pb2.h:293
@ VOICE_ASSISTANT_WAKE_WORD_START
Definition api_pb2.h:282
@ VOICE_ASSISTANT_TTS_STREAM_END
Definition api_pb2.h:287
@ VOICE_ASSISTANT_STT_VAD_START
Definition api_pb2.h:284
@ VOICE_ASSISTANT_INTENT_PROGRESS
Definition api_pb2.h:288
@ VOICE_ASSISTANT_TTS_STREAM_START
Definition api_pb2.h:286
@ VOICE_ASSISTANT_WAKE_WORD_END
Definition api_pb2.h:283
constexpr float AFTER_CONNECTION
For components that should be initialized after a data connection (API/MQTT) is connected.
Definition component.h:57
std::unique_ptr< Socket > socket_ip(int type, int protocol)
Create a socket in the newest available IP domain (IPv6 or IPv4) of the given type and protocol.
std::unique_ptr< Socket > socket(int domain, int type, int protocol)
Create a socket of the given domain, type and protocol.
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port)
Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
ESPHOME_LOG_TAG(TAG, "voice_assistant")
VoiceAssistant * global_voice_assistant
int written
Definition helpers.h:1262
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
std::vector< WakeWord > available_wake_words
std::vector< std::string > active_wake_words
static constexpr size_t TO_STR_BUFFER_SIZE
Buffer size for to_str() - sufficient for typical timer names.
std::vector< std::string > trained_languages
sa_family_t ss_family
Definition headers.h:104