ESPHome 2026.6.2
Loading...
Searching...
No Matches
resampler_speaker.cpp
Go to the documentation of this file.
1#include "resampler_speaker.h"
2
3#ifdef USE_ESP32
4
6
10#include "esphome/core/log.h"
11
12#include <algorithm>
13#include <cstring>
14
16
17static const UBaseType_t RESAMPLER_TASK_PRIORITY = 1;
18
19static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
20
21static const uint32_t TASK_STACK_SIZE = 3072;
22
23static const uint32_t STATE_TRANSITION_TIMEOUT_MS = 5000;
24
25static const char *const TAG = "resampler_speaker";
26
28 COMMAND_STOP = (1 << 0), // signals stop request
29 COMMAND_START = (1 << 1), // signals start request
30 COMMAND_FINISH = (1 << 2), // signals finish request (graceful stop)
31 TASK_COMMAND_STOP = (1 << 5), // signals the task to stop
32 STATE_STARTING = (1 << 10),
33 STATE_RUNNING = (1 << 11),
34 STATE_STOPPING = (1 << 12),
35 STATE_STOPPED = (1 << 13),
36 ERR_ESP_NO_MEM = (1 << 19),
38 ERR_ESP_FAIL = (1 << 21),
39 ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
40};
41
44 ESP_LOGCONFIG(TAG,
45 "Resampler Speaker:\n"
46 " Target Bits Per Sample: passthrough\n"
47 " Target Sample Rate: %" PRIu32 " Hz",
49 } else {
50 ESP_LOGCONFIG(TAG,
51 "Resampler Speaker:\n"
52 " Target Bits Per Sample: %" PRIu8 "\n"
53 " Target Sample Rate: %" PRIu32 " Hz",
55 }
56}
57
59 this->event_group_ = xEventGroupCreate();
60 if (this->event_group_ == nullptr) {
61 ESP_LOGE(TAG, "Failed to create event group");
62 this->mark_failed();
63 return;
64 }
65
66 this->output_speaker_->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) {
67 if (this->audio_stream_info_.get_sample_rate() != this->target_stream_info_.get_sample_rate()) {
68 // Convert the number of frames from the target sample rate to the source sample rate. Track the remainder to
69 // avoid losing frames from integer division truncation.
70 const uint64_t numerator = new_frames * this->audio_stream_info_.get_sample_rate() + this->callback_remainder_;
71 const uint64_t denominator = this->target_stream_info_.get_sample_rate();
72 this->callback_remainder_ = numerator % denominator;
73 this->audio_output_callback_(numerator / denominator, write_timestamp);
74 } else {
75 this->audio_output_callback_(new_frames, write_timestamp);
76 }
77 });
78
79 // Start with loop disabled since no task is running and no commands are pending
80 this->disable_loop();
81}
82
84 uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
85
86 // Process commands with priority: STOP > FINISH > START
87 // This ensures stop commands take precedence over conflicting start commands
88 if (event_group_bits & ResamplingEventGroupBits::COMMAND_STOP) {
90 // Clear STOP, START, and FINISH bits - stop takes precedence
91 xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP |
94 this->waiting_for_output_ = false;
96 } else if (this->state_ == speaker::STATE_STOPPED) {
97 // Already stopped, just clear the command bits
98 xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP |
101 }
102 // Leave bits set if STATE_STOPPING - will be processed once stopped
103 } else if (event_group_bits & ResamplingEventGroupBits::COMMAND_FINISH) {
104 if (this->state_ == speaker::STATE_RUNNING) {
105 xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_FINISH);
106 this->output_speaker_->finish();
107 } else if (this->state_ == speaker::STATE_STOPPED) {
108 // Already stopped, just clear the command bit
109 xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_FINISH);
110 }
111 // Leave bit set if transitioning states - will be processed once state allows
112 } else if (event_group_bits & ResamplingEventGroupBits::COMMAND_START) {
113 if (this->state_ == speaker::STATE_STOPPED) {
114 xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_START);
116 } else if (this->state_ == speaker::STATE_RUNNING) {
117 // Already running, just clear the command bit
118 xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_START);
119 }
120 // Leave bit set if transitioning states - will be processed once state allows
121 }
122
123 // Re-read bits after command processing (enter_stopping_state_ may have set task bits)
124 event_group_bits = xEventGroupGetBits(this->event_group_);
125
126 if (event_group_bits & ResamplingEventGroupBits::STATE_STARTING) {
127 ESP_LOGD(TAG, "Starting");
128 xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STARTING);
129 }
130
131 if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) {
132 this->status_set_error(LOG_STR("Not enough memory"));
133 xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM);
134 this->enter_stopping_state_();
135 }
137 this->status_set_error(LOG_STR("Unsupported stream"));
138 xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED);
139 this->enter_stopping_state_();
140 }
141 if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) {
142 this->status_set_error(LOG_STR("Resampler failure"));
143 xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL);
144 this->enter_stopping_state_();
145 }
146
147 if (event_group_bits & ResamplingEventGroupBits::STATE_RUNNING) {
148 ESP_LOGV(TAG, "Started");
149 this->status_clear_error();
150 xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_RUNNING);
151 }
152 if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPING) {
153 ESP_LOGV(TAG, "Stopping");
154 xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
155 }
156 if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) {
157 this->task_.deallocate();
158 ESP_LOGD(TAG, "Stopped");
159 xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS);
160 }
161
162 switch (this->state_) {
164 if (!this->waiting_for_output_) {
165 esp_err_t err = this->start_();
166 if (err == ESP_OK) {
167 this->callback_remainder_ = 0; // reset callback remainder
168 this->status_clear_error();
169 this->waiting_for_output_ = true;
171 } else {
172 this->set_start_error_(err);
173 this->waiting_for_output_ = false;
174 this->enter_stopping_state_();
175 }
176 } else {
177 if (this->output_speaker_->is_running()) {
179 this->waiting_for_output_ = false;
180 } else if ((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS) {
181 // Timed out waiting for the output speaker to start
182 this->waiting_for_output_ = false;
183 this->enter_stopping_state_();
184 }
185 }
186 break;
187 }
189 if (this->output_speaker_->is_stopped()) {
190 this->enter_stopping_state_();
191 }
192 break;
194 if ((this->output_speaker_->get_pause_state()) ||
195 ((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS)) {
196 // If output speaker is paused or stopping timeout exceeded, force stop
197 this->output_speaker_->stop();
198 }
199
200 if (this->output_speaker_->is_stopped() && !this->task_.is_created()) {
201 // Only transition to stopped state once the output speaker and resampler task are fully stopped
202 this->waiting_for_output_ = false;
204 }
205 break;
206 }
208 event_group_bits = xEventGroupGetBits(this->event_group_);
209 if (event_group_bits == 0) {
210 // No pending events, disable loop to save CPU cycles
211 this->disable_loop();
212 }
213 break;
214 }
215}
216
218 switch (err) {
219 case ESP_ERR_NO_MEM:
220 this->status_set_error(LOG_STR("Not enough memory"));
221 break;
222 default:
223 this->status_set_error(LOG_STR("Failed to start"));
224 break;
225 }
226}
227
228size_t ResamplerSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
229 if (this->is_stopped()) {
230 this->start();
231 }
232
233 size_t bytes_written = 0;
234 if ((this->output_speaker_->is_running()) && (!this->requires_resampling_())) {
235 bytes_written = this->output_speaker_->play(data, length, ticks_to_wait);
236 } else {
237 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
238 if (temp_ring_buffer) {
239 // Only write to the ring buffer if the reference is valid
240 bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
241 } else {
242 // Delay to avoid repeatedly hammering while waiting for the speaker to start
243 vTaskDelay(ticks_to_wait);
244 }
245 }
246
247 return bytes_written;
248}
249
250void ResamplerSpeaker::send_command_(uint32_t command_bit, bool wake_loop) {
252 uint32_t event_bits = xEventGroupGetBits(this->event_group_);
253 if (!(event_bits & command_bit)) {
254 xEventGroupSetBits(this->event_group_, command_bit);
255 if (wake_loop) {
257 }
258 }
259}
260
262
264 // In passthrough mode, the output keeps the input's bits per sample so only the sample rate is resampled.
265 const uint8_t target_bits_per_sample = this->passthrough_bits_per_sample_
268 this->target_stream_info_ = audio::AudioStreamInfo(target_bits_per_sample, this->audio_stream_info_.get_channels(),
269 this->target_sample_rate_);
270
272 this->output_speaker_->start();
273
274 if (this->requires_resampling_()) {
275 // Start the resampler task to handle converting sample rates
276 if (!this->task_.create(resample_task, "resampler", TASK_STACK_SIZE, (void *) this, RESAMPLER_TASK_PRIORITY,
277 this->task_stack_in_psram_)) {
278 return ESP_ERR_NO_MEM;
279 }
280 }
281
282 return ESP_OK;
283}
284
286
295
297
299 bool has_ring_buffer_data = false;
300 if (this->requires_resampling_()) {
301 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
302 if (temp_ring_buffer) {
303 has_ring_buffer_data = (temp_ring_buffer->available() > 0);
304 }
305 }
306 return (has_ring_buffer_data || this->output_speaker_->has_buffered_data());
307}
308
309void ResamplerSpeaker::set_mute_state(bool mute_state) {
310 this->mute_state_ = mute_state;
311 this->output_speaker_->set_mute_state(mute_state);
312}
313
315 this->volume_ = volume;
316 this->output_speaker_->set_volume(volume);
317}
318
320 if (this->audio_stream_info_.get_sample_rate() != this->target_sample_rate_) {
321 return true;
322 }
323 // In passthrough mode the bits per sample always matches the input, so it never forces resampling.
324 return !this->passthrough_bits_per_sample_ &&
326}
327
329 ResamplerSpeaker *this_resampler = static_cast<ResamplerSpeaker *>(params);
330
331 xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STARTING);
332
333 { // Ensure C++ objects fall out of scope for proper cleanup before stopping the task
334 std::unique_ptr<audio::AudioResampler> resampler = make_unique<audio::AudioResampler>(
335 this_resampler->audio_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS),
336 this_resampler->target_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
337
338 esp_err_t err = resampler->start(this_resampler->audio_stream_info_, this_resampler->target_stream_info_,
339 this_resampler->taps_, this_resampler->filters_);
340
341 if (err == ESP_OK) {
342 std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(
343 this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_));
344
345 if (!temp_ring_buffer) {
346 err = ESP_ERR_NO_MEM;
347 } else {
348 this_resampler->ring_buffer_ = temp_ring_buffer;
349 resampler->add_source(this_resampler->ring_buffer_);
350
351 this_resampler->output_speaker_->set_audio_stream_info(this_resampler->target_stream_info_);
352 resampler->add_sink(this_resampler->output_speaker_);
353 }
354 }
355
356 if (err == ESP_OK) {
357 xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_RUNNING);
358 } else if (err == ESP_ERR_NO_MEM) {
359 xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM);
360 } else if (err == ESP_ERR_NOT_SUPPORTED) {
361 xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED);
362 }
363
364 while (err == ESP_OK) {
365 uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_);
366
368 break;
369 }
370
371 // Stop gracefully if the decoder is done
372 int32_t ms_differential = 0;
373 audio::AudioResamplerState resampler_state = resampler->resample(false, &ms_differential);
374
375 if (resampler_state == audio::AudioResamplerState::FINISHED) {
376 break;
377 } else if (resampler_state == audio::AudioResamplerState::FAILED) {
378 xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL);
379 break;
380 }
381 }
382
383 xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
384 }
385
386 xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPED);
387
388 vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
389}
390
391} // namespace esphome::resampler
392
393#endif
void wake_loop_threadsafe()
Wake the main event loop from another thread or callback.
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void mark_failed()
Mark this component as failed.
void status_clear_error()
Definition component.h:295
void enable_loop_soon_any_context()
Thread and ISR-safe version of enable_loop() that can be called from any context.
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.
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
uint32_t get_sample_rate() const
Definition audio.h:30
void set_start_error_(esp_err_t err)
Sets the appropriate status error based on the start failure reason.
void set_volume(float volume) override
Volume state changes are passed to the parent's output speaker.
audio::AudioStreamInfo target_stream_info_
esp_err_t start_()
Starts the output speaker after setting the resampled stream info.
size_t play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) override
void enter_stopping_state_()
Transitions to STATE_STOPPING, records the stopping timestamp, sends the task stop command if the tas...
std::weak_ptr< ring_buffer::RingBuffer > ring_buffer_
void send_command_(uint32_t command_bit, bool wake_loop=false)
Sends a command via event group bits, enables the loop, and optionally wakes the main loop.
void set_mute_state(bool mute_state) override
Mute state changes are passed to the parent's output speaker.
static std::unique_ptr< RingBuffer > create(size_t len, MemoryPreference preference=MemoryPreference::EXTERNAL_FIRST)
virtual size_t play(const uint8_t *data, size_t length)=0
Plays the provided audio data.
bool is_running() const
Definition speaker.h:65
virtual void set_volume(float volume)
Definition speaker.h:70
virtual bool get_pause_state() const
Definition speaker.h:61
CallbackManager< void(uint32_t, int64_t)> audio_output_callback_
Definition speaker.h:122
void set_audio_stream_info(const audio::AudioStreamInfo &audio_stream_info)
Definition speaker.h:98
void add_audio_output_callback(F &&callback)
Callback function for sending the duration of the audio written to the speaker since the last callbac...
Definition speaker.h:108
virtual void set_mute_state(bool mute_state)
Definition speaker.h:80
virtual bool has_buffered_data() const =0
audio::AudioStreamInfo audio_stream_info_
Definition speaker.h:114
virtual void start()=0
virtual void finish()
Definition speaker.h:57
bool is_stopped() const
Definition speaker.h:66
virtual void stop()=0
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
uint16_t length
Definition tt21100.cpp:0