ESPHome 2026.5.0b1
Loading...
Searching...
No Matches
audio_file_media_source.cpp
Go to the documentation of this file.
2
3#ifdef USE_ESP32
4
5#include "esphome/core/log.h"
6
7#include <freertos/FreeRTOS.h>
8#include <freertos/task.h>
9
10#include <cstring>
11
12namespace esphome::audio_file {
13
14static const char *const TAG = "audio_file_media_source";
15
16static constexpr uint32_t AUDIO_WRITE_TIMEOUT_MS = 50;
17static constexpr size_t DECODER_TASK_STACK_SIZE = 5120;
18static constexpr uint8_t DECODER_TASK_PRIORITY = 2;
19static constexpr uint32_t PAUSE_POLL_DELAY_MS = 20;
20static constexpr char URI_PREFIX[] = "audio-file://";
21
22namespace { // anonymous namespace for internal linkage
23
24// audio::AudioFileType and micro_decoder::AudioFileType use different numeric layouts (audio's
25// values shift with USE_AUDIO_*_SUPPORT defines; micro_decoder's are fixed and guarded by
26// MICRO_DECODER_CODEC_*). The codec request flow in audio/__init__.py keeps the two sets of
27// guards aligned, so a switch with matching #ifdefs covers all reachable cases.
28micro_decoder::AudioFileType to_micro_decoder_type(audio::AudioFileType type) {
29 switch (type) {
30#ifdef USE_AUDIO_FLAC_SUPPORT
32 return micro_decoder::AudioFileType::FLAC;
33#endif
34#ifdef USE_AUDIO_MP3_SUPPORT
36 return micro_decoder::AudioFileType::MP3;
37#endif
38#ifdef USE_AUDIO_OPUS_SUPPORT
40 return micro_decoder::AudioFileType::OPUS;
41#endif
42#ifdef USE_AUDIO_WAV_SUPPORT
44 return micro_decoder::AudioFileType::WAV;
45#endif
46 default:
47 return micro_decoder::AudioFileType::NONE;
48 }
49}
50
51} // namespace
52
54 ESP_LOGCONFIG(TAG,
55 "Audio File Media Source:\n"
56 " Decoder Task Stack in PSRAM: %s",
57 YESNO(this->decoder_task_stack_in_psram_));
58}
59
61 this->disable_loop();
62
63 micro_decoder::DecoderConfig config;
64 config.audio_write_timeout_ms = AUDIO_WRITE_TIMEOUT_MS;
65 config.decoder_priority = DECODER_TASK_PRIORITY;
66 config.decoder_stack_size = DECODER_TASK_STACK_SIZE;
67 config.decoder_stack_in_psram = this->decoder_task_stack_in_psram_;
68
69 this->decoder_ = std::make_unique<micro_decoder::DecoderSource>(config);
70 if (this->decoder_ == nullptr) {
71 ESP_LOGE(TAG, "Failed to allocate decoder");
72 this->mark_failed();
73 return;
74 }
75 this->decoder_->set_listener(this);
76}
77
78void AudioFileMediaSource::loop() { this->decoder_->loop(); }
79
80bool AudioFileMediaSource::can_handle(const std::string &uri) const { return uri.starts_with(URI_PREFIX); }
81
82// Called from the orchestrator's main loop, so no synchronization needed with loop()
83bool AudioFileMediaSource::play_uri(const std::string &uri) {
84 if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener()) {
85 return false;
86 }
87
89 ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str());
90 return false;
91 }
92
93 if (!uri.starts_with(URI_PREFIX)) {
94 ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
95 return false;
96 }
97
98 const char *file_id = uri.c_str() + sizeof(URI_PREFIX) - 1;
99 this->current_file_ = nullptr;
100 for (const auto &named_file : get_named_audio_files()) {
101 if (strcmp(named_file.file_id, file_id) == 0) {
102 this->current_file_ = named_file.file;
103 break;
104 }
105 }
106
107 if (this->current_file_ == nullptr) {
108 ESP_LOGE(TAG, "Unknown file: '%s'", file_id);
109 return false;
110 }
111
112 micro_decoder::AudioFileType type = to_micro_decoder_type(this->current_file_->file_type);
113 if (this->decoder_->play_buffer(this->current_file_->data, this->current_file_->length, type)) {
114 this->pause_.store(false, std::memory_order_relaxed);
115 this->enable_loop();
116 return true;
117 }
118
119 ESP_LOGE(TAG, "Failed to start playback of '%s'", file_id);
120 return false;
121}
122
123// Called from the orchestrator's main loop, so no synchronization needed with loop()
125 switch (command) {
127 this->decoder_->stop();
128 break;
130 // Only valid while actively playing; ignoring from IDLE/ERROR/PAUSED prevents the state
131 // machine from getting stuck in PAUSED when no playback is active (which would block the
132 // next play_uri() call via its IDLE-state precondition).
134 break;
135 // PAUSE does not stop the decoder task. Instead, on_audio_write() returns 0 and temporarily
136 // yields, which fills any internal buffering and applies back pressure that effectively
137 // pauses the decoder task.
139 this->pause_.store(true, std::memory_order_relaxed);
140 break;
143 break;
145 this->pause_.store(false, std::memory_order_relaxed);
146 break;
147 default:
148 break;
149 }
150}
151
152// Called from the decoder task. Forwards to the orchestrator's listener, which is responsible for
153// being thread-safe with respect to its own audio writer.
154size_t AudioFileMediaSource::on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) {
155 if (this->pause_.load(std::memory_order_relaxed)) {
156 vTaskDelay(pdMS_TO_TICKS(PAUSE_POLL_DELAY_MS));
157 return 0;
158 }
159 return this->write_output(data, length, timeout_ms, this->stream_info_);
160}
161
162// Called from the decoder task before the first on_audio_write().
163void AudioFileMediaSource::on_stream_info(const micro_decoder::AudioStreamInfo &info) {
164 this->stream_info_ = audio::AudioStreamInfo(info.get_bits_per_sample(), info.get_channels(), info.get_sample_rate());
165}
166
167// microDecoder invokes on_state_change() from inside decoder_->loop(), so this runs on the main
168// loop thread and it's safe to call set_state_() directly.
169void AudioFileMediaSource::on_state_change(micro_decoder::DecoderState state) {
170 switch (state) {
171 case micro_decoder::DecoderState::IDLE:
173 this->disable_loop();
174 break;
175 case micro_decoder::DecoderState::PLAYING:
177 break;
178 case micro_decoder::DecoderState::FAILED:
180 break;
181 default:
182 break;
183 }
184}
185
186} // namespace esphome::audio_file
187
188#endif // USE_ESP32
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:284
bool is_ready() const
void enable_loop()
Enable this component's loop.
Definition component.h:258
bool status_has_error() const
Definition component.h:292
void disable_loop()
Disable this component's loop.
bool can_handle(const std::string &uri) const override
void on_state_change(micro_decoder::DecoderState state) override
void handle_command(media_source::MediaSourceCommand command) override
bool play_uri(const std::string &uri) override
std::unique_ptr< micro_decoder::DecoderSource > decoder_
size_t on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) override
void on_stream_info(const micro_decoder::AudioStreamInfo &info) override
void set_state_(MediaSourceState state)
Update state and notify listener This is the only way to change state_, ensuring listener notificatio...
MediaSourceState get_state() const
Get current playback state.
size_t write_output(const uint8_t *data, size_t length, uint32_t timeout_ms, const audio::AudioStreamInfo &stream_info)
Write audio data to the listener.
bool has_listener() const
Check if a listener has been registered.
uint16_t type
bool state
Definition fan.h:2
const StaticVector< NamedAudioFile, AUDIO_FILE_MAX_FILES > & get_named_audio_files()
Definition audio_file.h:24
MediaSourceCommand
Commands that are sent from the orchestrator to a media source.
static void uint32_t
AudioFileType file_type
Definition audio.h:126
uint16_t length
Definition tt21100.cpp:0