ESPHome 2026.3.0
Loading...
Searching...
No Matches
audio_file_media_source.cpp
Go to the documentation of this file.
2
3#ifdef USE_ESP32
4
6
7#include <cstring>
8
9namespace esphome::audio_file {
10
11namespace { // anonymous namespace for internal linkage
12struct AudioSinkAdapter : public audio::AudioSinkCallback {
13 media_source::MediaSource *source;
14 audio::AudioStreamInfo stream_info;
15
16 size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) override {
17 return this->source->write_output(data, length, pdTICKS_TO_MS(ticks_to_wait), this->stream_info);
18 }
19};
20} // namespace
21
22#if defined(USE_AUDIO_OPUS_SUPPORT)
23static constexpr uint32_t DECODE_TASK_STACK_SIZE = 5 * 1024;
24#else
25static constexpr uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024;
26#endif
27
28static const char *const TAG = "audio_file_media_source";
29
31 // Requests to start playback (set by play_uri, handled by loop)
32 REQUEST_START = (1 << 0),
33 // Commands from main loop to decode task
34 COMMAND_STOP = (1 << 1),
35 COMMAND_PAUSE = (1 << 2),
36 // Decode task lifecycle signals (one-shot, cleared by loop)
37 TASK_STARTING = (1 << 7),
38 TASK_RUNNING = (1 << 8),
39 TASK_STOPPING = (1 << 9),
40 TASK_STOPPED = (1 << 10),
41 TASK_ERROR = (1 << 11),
42 // Decode task state (level-triggered, set/cleared by decode task)
43 TASK_PAUSED = (1 << 12),
44 ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
45};
46
48 ESP_LOGCONFIG(TAG, "Audio File Media Source:");
49 ESP_LOGCONFIG(TAG, " Task Stack in PSRAM: %s", this->task_stack_in_psram_ ? "Yes" : "No");
50}
51
53 this->disable_loop();
54
55 this->event_group_ = xEventGroupCreate();
56 if (this->event_group_ == nullptr) {
57 ESP_LOGE(TAG, "Failed to create event group");
58 this->mark_failed();
59 return;
60 }
61}
62
64 EventBits_t event_bits = xEventGroupGetBits(this->event_group_);
65
66 if (event_bits & REQUEST_START) {
67 xEventGroupClearBits(this->event_group_, REQUEST_START);
69 }
70
71 switch (this->decoding_state_) {
73 if (!this->decode_task_.is_created()) {
74 xEventGroupClearBits(this->event_group_, ALL_BITS);
75 if (!this->decode_task_.create(decode_task, "AudioFileDec", DECODE_TASK_STACK_SIZE, this, 1,
76 this->task_stack_in_psram_)) {
77 ESP_LOGE(TAG, "Failed to create task");
78 this->status_momentary_error("task_create", 1000);
81 return;
82 }
83 }
85 break;
86 }
88 if (event_bits & TASK_STARTING) {
89 ESP_LOGD(TAG, "Starting");
90 xEventGroupClearBits(this->event_group_, TASK_STARTING);
91 }
92
93 if (event_bits & TASK_RUNNING) {
94 ESP_LOGV(TAG, "Started");
95 xEventGroupClearBits(this->event_group_, TASK_RUNNING);
97 }
98
99 if ((event_bits & TASK_PAUSED) && this->get_state() != media_source::MediaSourceState::PAUSED) {
101 } else if (!(event_bits & TASK_PAUSED) && this->get_state() == media_source::MediaSourceState::PAUSED) {
103 }
104
105 if (event_bits & TASK_STOPPING) {
106 ESP_LOGV(TAG, "Stopping");
107 xEventGroupClearBits(this->event_group_, TASK_STOPPING);
108 }
109
110 if (event_bits & TASK_ERROR) {
111 // Report error so the orchestrator knows playback failed; task will have already logged the specific error
113 }
114
115 if (event_bits & TASK_STOPPED) {
116 ESP_LOGD(TAG, "Stopped");
117 xEventGroupClearBits(this->event_group_, ALL_BITS);
118
119 this->decode_task_.deallocate();
122 }
123 break;
124 }
128 }
129 break;
130 }
131 }
132
135 this->disable_loop();
136 }
137}
138
139// Called from the orchestrator's main loop, so no synchronization needed with loop()
140bool AudioFileMediaSource::play_uri(const std::string &uri) {
141 if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener() ||
142 xEventGroupGetBits(this->event_group_) & REQUEST_START) {
143 return false;
144 }
145
146 // Check if source is already playing
148 ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str());
149 return false;
150 }
151
152 // Validate URI starts with "audio-file://"
153 if (!uri.starts_with("audio-file://")) {
154 ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
155 return false;
156 }
157
158 // Strip "audio-file://" prefix and find the file
159 const char *file_id = uri.c_str() + 13; // "audio-file://" is 13 characters
160
161 for (const auto &named_file : get_named_audio_files()) {
162 if (strcmp(named_file.file_id, file_id) == 0) {
163 this->current_file_ = named_file.file;
164 xEventGroupSetBits(this->event_group_, EventGroupBits::REQUEST_START);
165 this->enable_loop();
166 return true;
167 }
168 }
169
170 ESP_LOGE(TAG, "Unknown file: '%s'", file_id);
171 return false;
172}
173
174// Called from the orchestrator's main loop, so no synchronization needed with loop()
177 return;
178 }
179
180 switch (command) {
182 xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_STOP);
183 break;
185 xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_PAUSE);
186 break;
188 xEventGroupClearBits(this->event_group_, EventGroupBits::COMMAND_PAUSE);
189 break;
190 default:
191 break;
192 }
193}
194
196 AudioFileMediaSource *this_source = static_cast<AudioFileMediaSource *>(params);
197
198 do { // do-while(false) ensures RAII objects are destroyed on all exit paths via break
199
200 xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STARTING);
201
202 // 0 bytes for input transfer buffer makes it an inplace buffer
203 std::unique_ptr<audio::AudioDecoder> decoder = make_unique<audio::AudioDecoder>(0, 4096);
204
205 esp_err_t err = decoder->start(this_source->current_file_->file_type);
206 if (err != ESP_OK) {
207 ESP_LOGE(TAG, "Failed to start decoder: %s", esp_err_to_name(err));
209 break;
210 }
211
212 // Add the file as a const data source
213 decoder->add_source(this_source->current_file_->data, this_source->current_file_->length);
214
215 xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_RUNNING);
216
217 AudioSinkAdapter audio_sink;
218 bool has_stream_info = false;
219
220 while (true) {
221 EventBits_t event_bits = xEventGroupGetBits(this_source->event_group_);
222
223 if (event_bits & EventGroupBits::COMMAND_STOP) {
224 break;
225 }
226
227 bool paused = event_bits & EventGroupBits::COMMAND_PAUSE;
228 decoder->set_pause_output_state(paused);
229 if (paused) {
230 xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_PAUSED);
231 vTaskDelay(pdMS_TO_TICKS(20));
232 } else {
233 xEventGroupClearBits(this_source->event_group_, EventGroupBits::TASK_PAUSED);
234 }
235
236 // Will stop gracefully once finished with the current file
237 audio::AudioDecoderState decoder_state = decoder->decode(true);
238
239 if (decoder_state == audio::AudioDecoderState::FINISHED) {
240 break;
241 } else if (decoder_state == audio::AudioDecoderState::FAILED) {
242 ESP_LOGE(TAG, "Decoder failed");
243 xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR);
244 break;
245 }
246
247 if (!has_stream_info && decoder->get_audio_stream_info().has_value()) {
248 has_stream_info = true;
249
250 audio::AudioStreamInfo stream_info = decoder->get_audio_stream_info().value();
251
252 ESP_LOGD(TAG, "Bits per sample: %d, Channels: %d, Sample rate: %d", stream_info.get_bits_per_sample(),
253 stream_info.get_channels(), stream_info.get_sample_rate());
254
255 if (stream_info.get_bits_per_sample() != 16 || stream_info.get_channels() > 2) {
256 ESP_LOGE(TAG, "Incompatible audio stream. Only 16 bits per sample and 1 or 2 channels are supported");
257 xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR);
258 break;
259 }
260
261 audio_sink.source = this_source;
262 audio_sink.stream_info = stream_info;
263 esp_err_t err = decoder->add_sink(&audio_sink);
264 if (err != ESP_OK) {
265 ESP_LOGE(TAG, "Failed to add sink: %s", esp_err_to_name(err));
266 xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR);
267 break;
268 }
269 }
270 }
271
272 xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPING);
273 } while (false);
274
275 // All RAII objects from the do-while block (decoder, audio_sink, etc.) are now destroyed.
276
277 xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPED);
278 vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
279}
280
281} // namespace esphome::audio_file
282
283#endif // USE_ESP32
media_source::MediaSource * source
audio::AudioStreamInfo stream_info
void mark_failed()
Mark this component as failed.
void status_momentary_error(const char *name, uint32_t length=5000)
Set error status flag and automatically clear it after a timeout.
bool is_failed() const
Definition component.h:233
bool is_ready() const
void enable_loop()
Enable this component's loop.
bool status_has_error() const
Definition component.h:241
void disable_loop()
Disable this component's loop.
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:18
void deallocate()
Delete the task (if running) and free the stack buffer.
void handle_command(media_source::MediaSourceCommand command) override
bool play_uri(const std::string &uri) 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.
bool has_listener() const
Check if a listener has been registered.
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
const uint8_t * data
Definition audio.h:123
AudioFileType file_type
Definition audio.h:125
uint16_t length
Definition tt21100.cpp:0