ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
audio_pipeline.cpp
Go to the documentation of this file.
1#include "audio_pipeline.h"
2
3#ifdef USE_ESP32
4
6#include "esphome/core/hal.h"
8#include "esphome/core/log.h"
9
10namespace esphome::speaker {
11
12static const uint32_t INITIAL_BUFFER_MS = 1000; // Start playback after buffering this duration of the file
13
14static const uint32_t READ_TASK_STACK_SIZE = 5 * 1024;
15// Opus decoding uses more stack than other codecs
16#ifdef USE_AUDIO_OPUS_SUPPORT
17static const uint32_t DECODE_TASK_STACK_SIZE = 5 * 1024;
18#else
19static const uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024;
20#endif
21
22static const uint32_t INFO_ERROR_QUEUE_COUNT = 5;
23
24ESPHOME_LOG_TAG(TAG, "speaker_media_player.pipeline");
25
27 // MESSAGE_* bits are only set by their respective tasks
28
29 // Stops all activity in the pipeline elements; cleared by process_state() and set by stop() or by each task
31
32 // Read audio from an HTTP source; cleared by reader task and set by start_url
34 // Read audio from an audio file from the flash; cleared by reader task and set by start_file
36
37 // Audio file type is read after checking it is supported; cleared by decoder task
39 // Reader is done (either through a failure or just end of the stream); cleared by reader task
41 // Error reading the file; cleared by process_state()
43
44 // Decoder is done (either through a faiilure or the end of the stream); cleared by decoder task
46 // Error decoding the file; cleared by process_state() by decoder task
48};
49
50AudioPipeline::AudioPipeline(speaker::Speaker *speaker, size_t buffer_size, bool task_stack_in_psram,
51 const char *base_name, UBaseType_t priority)
52 : base_name_(base_name),
53 priority_(priority),
54 task_stack_in_psram_(task_stack_in_psram),
55 speaker_(speaker),
56 buffer_size_(buffer_size) {
58 this->transfer_buffer_size_ = std::min(buffer_size_ / 4, DEFAULT_TRANSFER_BUFFER_SIZE);
59}
60
61void AudioPipeline::start_url(const std::string &uri) {
62 if (this->is_playing_) {
63 xEventGroupSetBits(this->event_group_, PIPELINE_COMMAND_STOP);
64 }
65 this->current_uri_ = uri;
66 this->pending_url_ = true;
67}
68
70 if (this->is_playing_) {
71 xEventGroupSetBits(this->event_group_, PIPELINE_COMMAND_STOP);
72 }
73 this->current_audio_file_ = audio_file;
74 this->pending_file_ = true;
75}
76
78 xEventGroupSetBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP);
79
80 return ESP_OK;
81}
82void AudioPipeline::set_pause_state(bool pause_state) {
83 this->speaker_->set_pause_state(pause_state);
84
85 this->pause_state_ = pause_state;
86}
87
89 if (this->read_task_.is_created()) {
90 vTaskSuspend(this->read_task_.get_handle());
91 }
92 if (this->decode_task_.is_created()) {
93 vTaskSuspend(this->decode_task_.get_handle());
94 }
95}
96
98 if (this->read_task_.is_created()) {
99 vTaskResume(this->read_task_.get_handle());
100 }
101 if (this->decode_task_.is_created()) {
102 vTaskResume(this->decode_task_.get_handle());
103 }
104}
105
107 /*
108 * Log items from info error queue
109 */
110 InfoErrorEvent event;
111 if (this->info_error_queue_ != nullptr) {
112 while (xQueueReceive(this->info_error_queue_, &event, 0)) {
113 switch (event.source) {
115 if (event.err.has_value()) {
116 ESP_LOGE(TAG, "Media reader encountered an error: %s", esp_err_to_name(event.err.value()));
117 } else if (event.file_type.has_value()) {
118 ESP_LOGD(TAG, "Reading %s file type", audio_file_type_to_string(event.file_type.value()));
119 }
120
121 break;
123 if (event.err.has_value()) {
124 ESP_LOGE(TAG, "Decoder encountered an error: %s", esp_err_to_name(event.err.value()));
125 }
126
127 if (event.audio_stream_info.has_value()) {
128 ESP_LOGD(TAG, "Decoded audio has %d channels, %" PRId32 " Hz sample rate, and %d bits per sample",
129 event.audio_stream_info.value().get_channels(), event.audio_stream_info.value().get_sample_rate(),
130 event.audio_stream_info.value().get_bits_per_sample());
131 }
132
133 if (event.decoding_err.has_value()) {
134 switch (event.decoding_err.value()) {
136 ESP_LOGE(TAG, "Failed to parse the file's header.");
137 break;
139 ESP_LOGE(TAG, "Incompatible bits per sample. Only 16 bits per sample is supported");
140 break;
142 ESP_LOGE(TAG, "Incompatible number of channels. Only 1 or 2 channel audio is supported.");
143 break;
144 }
145 }
146 break;
147 }
148 }
149 }
150
151 /*
152 * Determine the current state based on the event group bits and tasks' status
153 */
154
155 EventBits_t event_bits = xEventGroupGetBits(this->event_group_);
156
157 if (this->pending_url_ || this->pending_file_) {
158 // Init command pending
159 if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) {
160 // Only start if there is no pending stop command
161 if (!this->read_task_.is_created() || !this->decode_task_.is_created()) {
162 // At least one task isn't running
163 this->start_tasks_();
164 }
165
166 if (this->pending_url_) {
168 this->playback_ms_ = 0;
169 this->pending_url_ = false;
170 } else if (this->pending_file_) {
172 this->playback_ms_ = 0;
173 this->pending_file_ = false;
174 }
175
176 this->is_playing_ = true;
178 }
179 }
180
181 if ((event_bits & EventGroupBits::READER_MESSAGE_ERROR)) {
182 xEventGroupClearBits(this->event_group_, EventGroupBits::READER_MESSAGE_ERROR);
184 }
185
186 if ((event_bits & EventGroupBits::DECODER_MESSAGE_ERROR)) {
187 xEventGroupClearBits(this->event_group_, EventGroupBits::DECODER_MESSAGE_ERROR);
189 }
190
191 if ((event_bits & EventGroupBits::READER_MESSAGE_FINISHED) &&
194 // Tasks are finished and there's no media in between the reader and decoder
195
196 if (event_bits & EventGroupBits::PIPELINE_COMMAND_STOP) {
197 // Stop command is fully processed, so clear the command bit
198 xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP);
199 this->hard_stop_ = true;
200 }
201
202 if (!this->is_playing_) {
203 // The tasks have been stopped for two ``process_state`` calls in a row, so delete the tasks
204 if (this->read_task_.is_created() || this->decode_task_.is_created()) {
205 // Both are attempted every time; a task that is still running on the other core is freed by a
206 // subsequent call, and freeing an already freed task succeeds without doing anything
207 bool read_task_freed = this->read_task_.deallocate();
208 bool decode_task_freed = this->decode_task_.deallocate();
209 if (!read_task_freed || !decode_task_freed) {
210 // A task is still running on the other core, so keep the pipeline in its current state and try
211 // again on the next call
213 }
214 if (this->hard_stop_) {
215 // Stop command was sent, so immediately end the playback
216 this->speaker_->stop();
217 this->hard_stop_ = false;
218 } else {
219 // Decoded all the audio, so let the speaker finish playing before stopping
220 this->speaker_->finish();
221 }
222 }
223 }
224 this->is_playing_ = false;
225 if (!this->speaker_->is_running()) {
227 } else {
228 this->is_finishing_ = true;
229 }
230 }
231
232 if (this->pause_state_) {
234 }
235
236 if (this->is_finishing_) {
237 if (!this->speaker_->is_running()) {
238 this->is_finishing_ = false;
239 } else {
241 }
242 }
243
244 if (!this->read_task_.is_created() && !this->decode_task_.is_created()) {
245 // No tasks are running, so the pipeline is stopped.
246 xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP);
248 }
249
250 this->is_playing_ = true;
252}
253
255 if (this->event_group_ == nullptr)
256 this->event_group_ = xEventGroupCreate();
257
258 if (this->event_group_ == nullptr) {
259 return ESP_ERR_NO_MEM;
260 }
261
262 if (this->info_error_queue_ == nullptr)
263 this->info_error_queue_ = xQueueCreate(INFO_ERROR_QUEUE_COUNT, sizeof(InfoErrorEvent));
264
265 if (this->info_error_queue_ == nullptr)
266 return ESP_ERR_NO_MEM;
267
268 return ESP_OK;
269}
270
272 char task_name[configMAX_TASK_NAME_LEN];
273 if (!this->read_task_.is_created()) {
274 // Reader task uses the AudioReader class which uses esp_http_client. This crashes on IDF 5.4 if the task stack is
275 // in PSRAM. As a workaround, always allocate the read task in internal memory.
276 snprintf(task_name, sizeof(task_name), "%s_read", this->base_name_);
277 if (!this->read_task_.create(read_task, task_name, READ_TASK_STACK_SIZE, (void *) this, this->priority_, false)) {
278 return ESP_ERR_NO_MEM;
279 }
280 }
281
282 if (!this->decode_task_.is_created()) {
283 snprintf(task_name, sizeof(task_name), "%s_decode", this->base_name_);
284 if (!this->decode_task_.create(decode_task, task_name, DECODE_TASK_STACK_SIZE, (void *) this, this->priority_,
285 this->task_stack_in_psram_)) {
286 return ESP_ERR_NO_MEM;
287 }
288 }
289
290 return ESP_OK;
291}
292
293void AudioPipeline::read_task(void *params) {
294 AudioPipeline *this_pipeline = (AudioPipeline *) params;
295
296 while (true) {
297 xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED);
298
299 // Wait until the pipeline notifies us the source of the media file
300 EventBits_t event_bits = xEventGroupWaitBits(
301 this_pipeline->event_group_,
303 pdFALSE, // Clear the bit on exit
304 pdFALSE, // Wait for all the bits,
305 portMAX_DELAY); // Block indefinitely until bit is set
306
307 if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) {
308 xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED |
311 InfoErrorEvent event;
313 esp_err_t err = ESP_OK;
314
315 std::unique_ptr<audio::AudioReader> reader =
316 make_unique<audio::AudioReader>(this_pipeline->transfer_buffer_size_);
317
319 err = reader->start(this_pipeline->current_audio_file_, this_pipeline->current_audio_file_type_);
320 } else {
321 err = reader->start(this_pipeline->current_uri_, this_pipeline->current_audio_file_type_);
322 }
323
324 if (err == ESP_OK) {
325 size_t file_ring_buffer_size = this_pipeline->buffer_size_;
326
327 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock();
328
329 if (temp_ring_buffer == nullptr) {
330 temp_ring_buffer = ring_buffer::RingBuffer::create(file_ring_buffer_size);
331 this_pipeline->raw_file_ring_buffer_ = temp_ring_buffer;
332 }
333
334 if (temp_ring_buffer == nullptr) {
335 err = ESP_ERR_NO_MEM;
336 } else {
337 err = reader->add_sink(temp_ring_buffer);
338 }
339 }
340
341 if (err != ESP_OK) {
342 // Send specific error message
343 event.err = err;
344 xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY);
345
346 // Setting up the reader failed, stop the pipeline
347 xEventGroupSetBits(this_pipeline->event_group_,
349 } else {
350 // Send the file type to the pipeline
351 event.file_type = this_pipeline->current_audio_file_type_;
352 xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY);
353 xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE);
354 }
355
356 while (true) {
357 event_bits = xEventGroupGetBits(this_pipeline->event_group_);
358
359 if (event_bits & EventGroupBits::PIPELINE_COMMAND_STOP) {
360 break;
361 }
362
363 audio::AudioReaderState reader_state = reader->read();
364
365 if (reader_state == audio::AudioReaderState::FINISHED) {
366 break;
367 } else if (reader_state == audio::AudioReaderState::FAILED) {
368 xEventGroupSetBits(this_pipeline->event_group_,
370 break;
371 }
372 }
373 event_bits = xEventGroupGetBits(this_pipeline->event_group_);
375 (this_pipeline->raw_file_ring_buffer_.use_count() == 1)) {
376 // Decoder task hasn't started yet, so delay a bit before releasing ownership of the ring buffer
377 delay(10);
378 }
379 }
380 }
381}
382
383void AudioPipeline::decode_task(void *params) {
384 AudioPipeline *this_pipeline = (AudioPipeline *) params;
385
386 while (true) {
387 xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED);
388
389 // Wait until the reader notifies us that the media type is available
390 EventBits_t event_bits =
391 xEventGroupWaitBits(this_pipeline->event_group_,
393 pdFALSE, // Clear the bit on exit
394 pdFALSE, // Wait for all the bits,
395 portMAX_DELAY); // Block indefinitely until bit is set
396
397 xEventGroupClearBits(this_pipeline->event_group_,
399
400 if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) {
401 InfoErrorEvent event;
403
404 std::unique_ptr<audio::AudioDecoder> decoder =
405 make_unique<audio::AudioDecoder>(this_pipeline->transfer_buffer_size_, this_pipeline->transfer_buffer_size_);
406
407 esp_err_t err = decoder->start(this_pipeline->current_audio_file_type_);
408 if (err == ESP_OK) {
409 err = decoder->add_source(this_pipeline->raw_file_ring_buffer_);
410 }
411
412 if (err != ESP_OK) {
413 // Send specific error message
414 event.err = err;
415 xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY);
416
417 // Setting up the decoder failed, stop the pipeline
418 xEventGroupSetBits(this_pipeline->event_group_,
420 }
421
422 bool has_stream_info = false;
423 bool started_playback = false;
424
425 size_t initial_bytes_to_buffer = 0;
426
427 while (true) {
428 event_bits = xEventGroupGetBits(this_pipeline->event_group_);
429
430 if (event_bits & EventGroupBits::PIPELINE_COMMAND_STOP) {
431 break;
432 }
433
434 // Update pause state
435 if (!started_playback) {
436 if (!(event_bits & EventGroupBits::READER_MESSAGE_FINISHED)) {
437 decoder->set_pause_output_state(true);
438 } else {
439 started_playback = true;
440 }
441 } else {
442 decoder->set_pause_output_state(this_pipeline->pause_state_);
443 }
444
445 // Stop gracefully if the reader has finished
446 audio::AudioDecoderState decoder_state = decoder->decode(event_bits & EventGroupBits::READER_MESSAGE_FINISHED);
447
448 if ((decoder_state == audio::AudioDecoderState::DECODING) ||
449 (decoder_state == audio::AudioDecoderState::FINISHED)) {
450 this_pipeline->playback_ms_ = decoder->get_playback_ms();
451 }
452
453 if (decoder_state == audio::AudioDecoderState::FINISHED) {
454 break;
455 } else if (decoder_state == audio::AudioDecoderState::FAILED) {
456 if (!has_stream_info) {
457 event.decoding_err = DecodingError::FAILED_HEADER;
458 xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY);
459 }
460 xEventGroupSetBits(this_pipeline->event_group_,
462 break;
463 }
464
465 if (!has_stream_info && decoder->get_audio_stream_info().has_value()) {
466 has_stream_info = true;
467
468 this_pipeline->current_audio_stream_info_ = decoder->get_audio_stream_info().value();
469
470 // Send the stream information to the pipeline
471 event.audio_stream_info = this_pipeline->current_audio_stream_info_;
472
473 if (this_pipeline->current_audio_stream_info_.get_bits_per_sample() != 16) {
474 // Error state, incompatible bits per sample
476 xEventGroupSetBits(this_pipeline->event_group_,
478 } else if ((this_pipeline->current_audio_stream_info_.get_channels() > 2)) {
479 // Error state, incompatible number of channels
480 event.decoding_err = DecodingError::INCOMPATIBLE_CHANNELS;
481 xEventGroupSetBits(this_pipeline->event_group_,
483 } else {
484 // Send audio directly to the speaker
485 this_pipeline->speaker_->set_audio_stream_info(this_pipeline->current_audio_stream_info_);
486 decoder->add_sink(this_pipeline->speaker_);
487 }
488
489 initial_bytes_to_buffer = std::min(this_pipeline->current_audio_stream_info_.ms_to_bytes(INITIAL_BUFFER_MS),
490 this_pipeline->buffer_size_ * 3 / 4);
491
492 switch (this_pipeline->current_audio_file_type_) {
493#ifdef USE_AUDIO_MP3_SUPPORT
495 initial_bytes_to_buffer /= 8; // Estimate the MP3 compression factor is 8
496 break;
497#endif
498#ifdef USE_AUDIO_FLAC_SUPPORT
500 initial_bytes_to_buffer /= 2; // Estimate the FLAC compression factor is 2
501 break;
502#endif
503#ifdef USE_AUDIO_OPUS_SUPPORT
505 initial_bytes_to_buffer /= 8; // Estimate the Opus compression factor is 8
506 break;
507#endif
508 default:
509 break;
510 }
511 xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY);
512 }
513
514 if (!started_playback && has_stream_info) {
515 // Verify enough data is available before starting playback
516 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock();
517 if (temp_ring_buffer != nullptr && temp_ring_buffer->available() >= initial_bytes_to_buffer) {
518 started_playback = true;
519 }
520 }
521 }
522 }
523 }
524}
525
526} // namespace esphome::speaker
527
528#endif
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.
size_t ms_to_bytes(uint32_t ms) const
Converts duration to bytes.
Definition audio.h:75
uint8_t get_bits_per_sample() const
Definition audio.h:28
uint8_t get_channels() const
Definition audio.h:29
static std::unique_ptr< RingBuffer > create(size_t len, MemoryPreference preference=MemoryPreference::EXTERNAL_FIRST)
const audio::AudioFile * current_audio_file_
static void read_task(void *params)
void suspend_tasks()
Suspends any running tasks.
void set_pause_state(bool pause_state)
void start_file(const audio::AudioFile *audio_file)
Starts an audio pipeline given a AudioFile pointer.
void start_url(const std::string &uri)
Starts an audio pipeline given a media url.
esp_err_t allocate_communications_()
Allocates the event group and info error queue.
esp_err_t start_tasks_()
Common start code for the pipeline, regardless if the source is a file or url.
audio::AudioStreamInfo current_audio_stream_info_
esp_err_t stop()
Stops the pipeline.
AudioPipeline(speaker::Speaker *speaker, size_t buffer_size, bool task_stack_in_psram, const char *base_name, UBaseType_t priority)
static void decode_task(void *params)
void resume_tasks()
Resumes any running tasks.
audio::AudioFileType current_audio_file_type_
std::weak_ptr< ring_buffer::RingBuffer > raw_file_ring_buffer_
AudioPipelineState process_state()
Processes the state of the audio pipeline based on the info_error_queue_ and event_group_.
bool is_running() const
Definition speaker.h:68
virtual void set_pause_state(bool pause_state)
Definition speaker.h:63
void set_audio_stream_info(const audio::AudioStreamInfo &audio_stream_info)
Definition speaker.h:97
virtual void finish()
Definition speaker.h:60
virtual void stop()=0
uint8_t priority
ESPHOME_LOG_TAG(TAG, "announcement_volume")
void HOT delay(uint32_t ms)
Definition hal.cpp:85
static void uint32_t
optional< DecodingError > decoding_err
optional< audio::AudioFileType > file_type
optional< audio::AudioStreamInfo > audio_stream_info