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