ESPHome 2026.10.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
24static const char *const 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 std::string base_name, UBaseType_t priority)
52 : base_name_(std::move(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 if (!this->read_task_.is_created()) {
273 // Reader task uses the AudioReader class which uses esp_http_client. This crashes on IDF 5.4 if the task stack is
274 // in PSRAM. As a workaround, always allocate the read task in internal memory.
275 if (!this->read_task_.create(read_task, (this->base_name_ + "_read").c_str(), READ_TASK_STACK_SIZE, (void *) this,
276 this->priority_, false)) {
277 return ESP_ERR_NO_MEM;
278 }
279 }
280
281 if (!this->decode_task_.is_created()) {
282 if (!this->decode_task_.create(decode_task, (this->base_name_ + "_decode").c_str(), DECODE_TASK_STACK_SIZE,
283 (void *) this, this->priority_, this->task_stack_in_psram_)) {
284 return ESP_ERR_NO_MEM;
285 }
286 }
287
288 return ESP_OK;
289}
290
291void AudioPipeline::read_task(void *params) {
292 AudioPipeline *this_pipeline = (AudioPipeline *) params;
293
294 while (true) {
295 xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED);
296
297 // Wait until the pipeline notifies us the source of the media file
298 EventBits_t event_bits = xEventGroupWaitBits(
299 this_pipeline->event_group_,
301 pdFALSE, // Clear the bit on exit
302 pdFALSE, // Wait for all the bits,
303 portMAX_DELAY); // Block indefinitely until bit is set
304
305 if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) {
306 xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED |
309 InfoErrorEvent event;
311 esp_err_t err = ESP_OK;
312
313 std::unique_ptr<audio::AudioReader> reader =
314 make_unique<audio::AudioReader>(this_pipeline->transfer_buffer_size_);
315
317 err = reader->start(this_pipeline->current_audio_file_, this_pipeline->current_audio_file_type_);
318 } else {
319 err = reader->start(this_pipeline->current_uri_, this_pipeline->current_audio_file_type_);
320 }
321
322 if (err == ESP_OK) {
323 size_t file_ring_buffer_size = this_pipeline->buffer_size_;
324
325 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock();
326
327 if (temp_ring_buffer == nullptr) {
328 temp_ring_buffer = ring_buffer::RingBuffer::create(file_ring_buffer_size);
329 this_pipeline->raw_file_ring_buffer_ = temp_ring_buffer;
330 }
331
332 if (temp_ring_buffer == nullptr) {
333 err = ESP_ERR_NO_MEM;
334 } else {
335 err = reader->add_sink(temp_ring_buffer);
336 }
337 }
338
339 if (err != ESP_OK) {
340 // Send specific error message
341 event.err = err;
342 xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY);
343
344 // Setting up the reader failed, stop the pipeline
345 xEventGroupSetBits(this_pipeline->event_group_,
347 } else {
348 // Send the file type to the pipeline
349 event.file_type = this_pipeline->current_audio_file_type_;
350 xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY);
351 xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE);
352 }
353
354 while (true) {
355 event_bits = xEventGroupGetBits(this_pipeline->event_group_);
356
357 if (event_bits & EventGroupBits::PIPELINE_COMMAND_STOP) {
358 break;
359 }
360
361 audio::AudioReaderState reader_state = reader->read();
362
363 if (reader_state == audio::AudioReaderState::FINISHED) {
364 break;
365 } else if (reader_state == audio::AudioReaderState::FAILED) {
366 xEventGroupSetBits(this_pipeline->event_group_,
368 break;
369 }
370 }
371 event_bits = xEventGroupGetBits(this_pipeline->event_group_);
373 (this_pipeline->raw_file_ring_buffer_.use_count() == 1)) {
374 // Decoder task hasn't started yet, so delay a bit before releasing ownership of the ring buffer
375 delay(10);
376 }
377 }
378 }
379}
380
381void AudioPipeline::decode_task(void *params) {
382 AudioPipeline *this_pipeline = (AudioPipeline *) params;
383
384 while (true) {
385 xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED);
386
387 // Wait until the reader notifies us that the media type is available
388 EventBits_t event_bits =
389 xEventGroupWaitBits(this_pipeline->event_group_,
391 pdFALSE, // Clear the bit on exit
392 pdFALSE, // Wait for all the bits,
393 portMAX_DELAY); // Block indefinitely until bit is set
394
395 xEventGroupClearBits(this_pipeline->event_group_,
397
398 if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) {
399 InfoErrorEvent event;
401
402 std::unique_ptr<audio::AudioDecoder> decoder =
403 make_unique<audio::AudioDecoder>(this_pipeline->transfer_buffer_size_, this_pipeline->transfer_buffer_size_);
404
405 esp_err_t err = decoder->start(this_pipeline->current_audio_file_type_);
406 if (err == ESP_OK) {
407 err = decoder->add_source(this_pipeline->raw_file_ring_buffer_);
408 }
409
410 if (err != ESP_OK) {
411 // Send specific error message
412 event.err = err;
413 xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY);
414
415 // Setting up the decoder failed, stop the pipeline
416 xEventGroupSetBits(this_pipeline->event_group_,
418 }
419
420 bool has_stream_info = false;
421 bool started_playback = false;
422
423 size_t initial_bytes_to_buffer = 0;
424
425 while (true) {
426 event_bits = xEventGroupGetBits(this_pipeline->event_group_);
427
428 if (event_bits & EventGroupBits::PIPELINE_COMMAND_STOP) {
429 break;
430 }
431
432 // Update pause state
433 if (!started_playback) {
434 if (!(event_bits & EventGroupBits::READER_MESSAGE_FINISHED)) {
435 decoder->set_pause_output_state(true);
436 } else {
437 started_playback = true;
438 }
439 } else {
440 decoder->set_pause_output_state(this_pipeline->pause_state_);
441 }
442
443 // Stop gracefully if the reader has finished
444 audio::AudioDecoderState decoder_state = decoder->decode(event_bits & EventGroupBits::READER_MESSAGE_FINISHED);
445
446 if ((decoder_state == audio::AudioDecoderState::DECODING) ||
447 (decoder_state == audio::AudioDecoderState::FINISHED)) {
448 this_pipeline->playback_ms_ = decoder->get_playback_ms();
449 }
450
451 if (decoder_state == audio::AudioDecoderState::FINISHED) {
452 break;
453 } else if (decoder_state == audio::AudioDecoderState::FAILED) {
454 if (!has_stream_info) {
455 event.decoding_err = DecodingError::FAILED_HEADER;
456 xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY);
457 }
458 xEventGroupSetBits(this_pipeline->event_group_,
460 break;
461 }
462
463 if (!has_stream_info && decoder->get_audio_stream_info().has_value()) {
464 has_stream_info = true;
465
466 this_pipeline->current_audio_stream_info_ = decoder->get_audio_stream_info().value();
467
468 // Send the stream information to the pipeline
469 event.audio_stream_info = this_pipeline->current_audio_stream_info_;
470
471 if (this_pipeline->current_audio_stream_info_.get_bits_per_sample() != 16) {
472 // Error state, incompatible bits per sample
474 xEventGroupSetBits(this_pipeline->event_group_,
476 } else if ((this_pipeline->current_audio_stream_info_.get_channels() > 2)) {
477 // Error state, incompatible number of channels
478 event.decoding_err = DecodingError::INCOMPATIBLE_CHANNELS;
479 xEventGroupSetBits(this_pipeline->event_group_,
481 } else {
482 // Send audio directly to the speaker
483 this_pipeline->speaker_->set_audio_stream_info(this_pipeline->current_audio_stream_info_);
484 decoder->add_sink(this_pipeline->speaker_);
485 }
486
487 initial_bytes_to_buffer = std::min(this_pipeline->current_audio_stream_info_.ms_to_bytes(INITIAL_BUFFER_MS),
488 this_pipeline->buffer_size_ * 3 / 4);
489
490 switch (this_pipeline->current_audio_file_type_) {
491#ifdef USE_AUDIO_MP3_SUPPORT
493 initial_bytes_to_buffer /= 8; // Estimate the MP3 compression factor is 8
494 break;
495#endif
496#ifdef USE_AUDIO_FLAC_SUPPORT
498 initial_bytes_to_buffer /= 2; // Estimate the FLAC compression factor is 2
499 break;
500#endif
501#ifdef USE_AUDIO_OPUS_SUPPORT
503 initial_bytes_to_buffer /= 8; // Estimate the Opus compression factor is 8
504 break;
505#endif
506 default:
507 break;
508 }
509 xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY);
510 }
511
512 if (!started_playback && has_stream_info) {
513 // Verify enough data is available before starting playback
514 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock();
515 if (temp_ring_buffer != nullptr && temp_ring_buffer->available() >= initial_bytes_to_buffer) {
516 started_playback = true;
517 }
518 }
519 }
520 }
521 }
522}
523
524} // namespace esphome::speaker
525
526#endif
bool create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority, bool use_psram)
Allocate stack and create task.
bool is_created() const
Check if the task has been created and not yet destroyed.
Definition static_task.h:19
TaskHandle_t get_handle() const
Get the FreeRTOS task handle.
Definition static_task.h:22
bool deallocate()
Delete the task (if created) and free the stack buffer.
size_t ms_to_bytes(uint32_t ms) const
Converts duration to bytes.
Definition audio.h:73
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)
static void read_task(void *params)
void suspend_tasks()
Suspends any running tasks.
void set_pause_state(bool pause_state)
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_
void start_file(audio::AudioFile *audio_file)
Starts an audio pipeline given a AudioFile pointer.
esp_err_t stop()
Stops the pipeline.
static void decode_task(void *params)
AudioPipeline(speaker::Speaker *speaker, size_t buffer_size, bool task_stack_in_psram, std::string base_name, UBaseType_t priority)
void resume_tasks()
Resumes any running tasks.
audio::AudioFile * current_audio_file_
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:65
virtual void set_pause_state(bool pause_state)
Definition speaker.h:60
void set_audio_stream_info(const audio::AudioStreamInfo &audio_stream_info)
Definition speaker.h:98
virtual void finish()
Definition speaker.h:57
virtual void stop()=0
uint8_t priority
void HOT delay(uint32_t ms)
Definition hal.cpp:85
STL namespace.
static void uint32_t
optional< DecodingError > decoding_err
optional< audio::AudioFileType > file_type
optional< audio::AudioStreamInfo > audio_stream_info